Compare commits

...

208 Commits

Author SHA1 Message Date
RuLang c8b4f3ec31 update_2022.07.04_small-fix 2022-07-04 11:39:15 +00:00
RuLang 3405859c8e intl_ru_15.06.2022.arb 2022-06-15 09:27:14 +00:00
Sarah Jamie Lewis 644ae502e5 Merge pull request 'formatting_toolbar' (#475) from formatting_toolbar into trunk
Reviewed-on: cwtch.im/cwtch-ui#475
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-15 03:14:17 +00:00
Sarah Jamie Lewis 7bae6485f7 Fixup Formatting PR (Dans Comments) 2022-06-14 18:44:24 -07:00
Sarah Jamie Lewis 04c335e7a4 formatting toolbar 2022-06-14 18:30:04 -07:00
Sarah Jamie Lewis 3961692817 Nicer Quoted Messages 2022-06-13 10:06:06 -07:00
Sarah Jamie Lewis d703a9636f Fix Contact Message Date not displaying date for day old messages 2022-06-13 09:31:25 -07:00
Dan Ballard e4419366a4 Merge pull request 'Click to scroll on Quoted Message / Shorten Text' (#469) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#469
2022-06-10 23:43:09 +00:00
Sarah Jamie Lewis f848316db9 Fix bug preventing scrolling to unread messages 2022-06-10 15:42:54 -07:00
Sarah Jamie Lewis a5b253f185 Merge pull request 'reply_links' (#470) from reply_links into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#470
2022-06-10 21:32:13 +00:00
Sarah Jamie Lewis e7c19c7477 Merge pull request 'show down button in messageview when ever scrolling up' (#471) from show_down into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#471
2022-06-10 21:31:10 +00:00
Dan Ballard 59df024867 show down button in messageview when ever scrolling up 2022-06-10 14:28:16 -07:00
Sarah Jamie Lewis 65ff084952 make links in replies clickable 2022-06-10 14:21:40 -07:00
Sarah Jamie Lewis b3e11cfffd remove scroll controller from message view 2022-06-10 12:24:38 -07:00
Sarah Jamie Lewis 0c9be47e17 Click to scroll on Quoted Message / Shorten Text 2022-06-10 12:12:43 -07:00
Dan Ballard 3bb3a8736c Merge pull request 'Fix message view title padding in doublecol view' (#468) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#468
2022-06-10 18:16:31 +00:00
Sarah Jamie Lewis 67850e8e4b Fix message view title padding in doublecol view 2022-06-10 10:40:39 -07:00
Dan Ballard c8e896fa51 Merge pull request 'Modal Menu UI Fixes' (#467) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#467
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 23:19:13 +00:00
Sarah Jamie Lewis d1e8f71290 fixes for profile buttons 2022-06-09 14:54:48 -07:00
Sarah Jamie Lewis be8646e805 fix padding 2022-06-09 14:30:38 -07:00
Sarah Jamie Lewis 6d42f2c76c make text bold and add additional padding to contacts modal 2022-06-09 14:28:24 -07:00
Sarah Jamie Lewis 8429907650 modal menus design fixes 2022-06-09 14:26:02 -07:00
Sarah Jamie Lewis c3848553d7 Bugfix when resizing app when menu is open 2022-06-09 13:49:38 -07:00
Sarah Jamie Lewis 3c85b8f59e Merge pull request 'Column-wise contact row (marcia feedback)' (#466) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#466
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 20:43:10 +00:00
Sarah Jamie Lewis d0e7e6703b Different buttons 2022-06-09 13:40:42 -07:00
Sarah Jamie Lewis 2bc47173f9 more clear contact request 2022-06-09 13:36:59 -07:00
Sarah Jamie Lewis 15c68d8812 remove padding 2022-06-09 13:20:01 -07:00
Sarah Jamie Lewis e76f2883c6 Column-wise contact row (marcia feedback) 2022-06-09 13:10:27 -07:00
Dan Ballard 439b9b874f Merge pull request 'marcia settings fixes' (#462) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#462
2022-05-31 23:31:27 +00:00
Sarah Jamie Lewis f5393cdb79 Merge branch 'trunk' into marcia_fixes 2022-05-31 23:19:49 +00:00
Sarah Jamie Lewis c0f1b674aa marcia settings fixes 2022-05-31 13:37:32 -07:00
Dan Ballard 630713a5e4 Merge pull request 'New Polish Translations' (#460) from pl_intl into trunk
Reviewed-on: cwtch.im/cwtch-ui#460
2022-05-24 18:53:52 +00:00
Sarah Jamie Lewis d10a6df872 Merge branch 'trunk' into pl_intl 2022-05-24 18:16:16 +00:00
Sarah Jamie Lewis 2723a35d44 New Polish Translations 2022-05-24 11:14:42 -07:00
Dan Ballard 427081c937 Merge pull request 'Fix #457 + Formatting' (#459) from fix457 into trunk
Reviewed-on: cwtch.im/cwtch-ui#459
2022-05-11 19:56:11 +00:00
Sarah Jamie Lewis 9d4abc3725 Fix #457 + Formatting 2022-05-11 12:44:24 -07:00
Sarah Jamie Lewis fa52b741bf Merge pull request 'v1.7.1 29' (#454) from pubspecBump into trunk
Reviewed-on: cwtch.im/cwtch-ui#454
2022-05-02 22:12:25 +00:00
Dan Ballard fb86fb6eae v1.7.1 29 2022-05-02 15:07:55 -07:00
Sarah Jamie Lewis 8dd696b6ab Merge pull request 'dont start 'new messages' when convo selected' (#453) from cache3.0 into trunk
Reviewed-on: cwtch.im/cwtch-ui#453
2022-05-01 17:32:16 +00:00
Dan Ballard 001ad854c7 dont start 'new messages' when convo selected 2022-04-30 14:43:45 -07:00
Dan Ballard af5fb678fc Merge pull request 'caching fixes for stability and android' (#450) from cache3.0 into trunk
Reviewed-on: cwtch.im/cwtch-ui#450
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-29 23:37:20 +00:00
Dan Ballard ffa51e83a1 new message marker moved from id to index and now works on old messages 2022-04-29 16:07:52 -07:00
Dan Ballard 441845ed49 Merge pull request 'Fix maximum width of dropdown boxes in settings' (#452) from fix-settings into trunk
Reviewed-on: cwtch.im/cwtch-ui#452
2022-04-29 17:50:18 +00:00
Sarah Jamie Lewis 0146436cb3 Fix maximum width of dropdown boxes in settings 2022-04-29 09:57:26 -07:00
Dan Ballard 0647a2d98d android pre load unsynced messages 2022-04-28 21:28:12 -07:00
Dan Ballard 0bcfe75a63 rework cache android resume based off message count totals, force pre fetch on load message list, tweak new messages bubble behaviour 2022-04-28 08:57:31 -07:00
Dan Ballard ecdcef2192 Merge pull request 'GetMessage* on android; make reply to use message cache; New Messages bubble doesn't reup' (#448) from replyFix into trunk
Reviewed-on: cwtch.im/cwtch-ui#448
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-27 04:51:20 +00:00
Dan Ballard e6c9f7becb GetMessage* on android; make reply to use message cache; New Messages bubble doesn't reup 2022-04-26 21:34:16 -07:00
Sarah Jamie Lewis 9d8f73ac00 Merge pull request 'Format, Context Binding and Check if File Exists in File Bubble' (#447) from file-fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#447
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-26 19:38:28 +00:00
Sarah Jamie Lewis dc78117e1a Format, Context Binding and Check if File Exists in File Bubble 2022-04-26 12:16:48 -07:00
Dan Ballard 59e3220bce Merge pull request 'Debug Info Fix and Dependency Upgrades' (#433) from perf into trunk
Reviewed-on: cwtch.im/cwtch-ui#433
2022-04-21 23:56:32 +00:00
Sarah Jamie Lewis 653ba199bc Merge branch 'trunk' into perf 2022-04-21 23:33:26 +00:00
Sarah Jamie Lewis 1b45205c48 Merge pull request 'nsis uninstall typo reg key' (#438) from winUninstall into trunk
Reviewed-on: cwtch.im/cwtch-ui#438
2022-04-21 23:33:17 +00:00
Dan Ballard 85186b2565 nsis uninstall typo reg key 2022-04-21 16:32:22 -07:00
Sarah Jamie Lewis 3287fa79ff Merge branch 'trunk' into perf 2022-04-21 23:32:07 +00:00
Sarah Jamie Lewis 111d522484 Upgrade lcg to 1.7.1 2022-04-21 16:31:17 -07:00
Sarah Jamie Lewis 20c854bafb Update Translations 2022-04-21 16:14:03 -07:00
Dan Ballard ffdc7b3262 Merge pull request 'winUninstall' (#434) from winUninstall into trunk
Reviewed-on: cwtch.im/cwtch-ui#434
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-21 18:40:31 +00:00
Dan Ballard a3d986d9d6 ffi on windows more options to detect tor; nsis installer warn about cwtch needing exiting 2022-04-20 18:20:44 -07:00
Sarah Jamie Lewis 5e3387ec8a Debug Info Fix an Dependency Upgrades 2022-04-20 17:28:38 -07:00
Dan Ballard a6c7682c84 nsis windows installer detect running, ask to not, abort 2022-04-20 14:25:36 -07:00
Dan Ballard b29836ed3b register uninstaller with windows add/remove programs 2022-04-20 12:26:28 -07:00
Sarah Jamie Lewis e0bf47b6ab Merge pull request 'a bunch of cache logic fixes and futher support for reconnect on android' (#431) from cachefixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#431
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-20 18:16:47 +00:00
Dan Ballard 4bd92d854f comments 2022-04-19 20:46:59 -07:00
Dan Ballard 82d1bf873f lcg bump 2022-04-19 20:46:59 -07:00
Dan Ballard 5959981fe4 a bunch of cache logic fixes and futher support for reconnect on android 2022-04-19 20:46:59 -07:00
Dan Ballard ab315e289a Merge pull request 'MainActivity return result to not leave dart calls hanging' (#432) from kotlinResult into trunk
Reviewed-on: cwtch.im/cwtch-ui#432
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-20 03:36:20 +00:00
Dan Ballard 6392d67332 MainActivity return result to not leave dart calls hanging 2022-04-19 18:34:22 -07:00
Dan Ballard 8f0b73af2a Merge pull request 'fix linux notification icon (rever to old linux notification manager) and light theme fixes' (#429) from linuxNotif into trunk
Reviewed-on: cwtch.im/cwtch-ui#429
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-15 01:02:30 +00:00
Dan Ballard 4e2f83ccd9 light theme fixes + message cache ! fix 2022-04-14 17:50:53 -07:00
Dan Ballard dc5ba7b392 readd linux notification manager so it handles notification icon in different linux style installs 2022-04-14 17:02:24 -07:00
Sarah Jamie Lewis 3595f5d8d1 Merge pull request 'Debug Info Pane for Desktop' (#428) from debuginfo into trunk
Reviewed-on: cwtch.im/cwtch-ui#428
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-14 22:51:35 +00:00
Sarah Jamie Lewis 1df348c0c1 Debug Info Pane for Desktop 2022-04-14 15:34:36 -07:00
Sarah Jamie Lewis 548e7f4925 Merge pull request 'add android flag secure, pubspec vewrsion bump, and stubs for sdk31 hide overlay' (#427) from androidFlags into trunk
Reviewed-on: cwtch.im/cwtch-ui#427
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-13 21:57:52 +00:00
Dan Ballard a20d2dffc4 add android flag secure, pubspec vewrsion bump, and stubs for sdk31 hide overlay 2022-04-13 14:53:44 -07:00
Dan Ballard 2a712565e9 Merge pull request 'andoird settings / request for power optimization exemption' (#426) from power into trunk
Reviewed-on: cwtch.im/cwtch-ui#426
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-13 21:23:41 +00:00
Dan Ballard a94fd3547b add popup about disable battery unoptimized; fix mute policy loading 2022-04-13 14:09:33 -07:00
Dan Ballard c377a09748 add setting that reports / triggers android power optimization exemption 2022-04-13 12:57:15 -07:00
Dan Ballard d261fbd4c0 kotlin powermanagement info and exemption request 2022-04-13 12:53:32 -07:00
Dan Ballard 933ca74fbc Merge pull request 'Themeing Updates including Nicer Code Formatting' (#425) from theme-updates into trunk
Reviewed-on: cwtch.im/cwtch-ui#425
2022-04-12 22:08:20 +00:00
Sarah Jamie Lewis 38f317194d Merge branch 'trunk' into theme-updates 2022-04-12 21:18:58 +00:00
Sarah Jamie Lewis a4ab2ec060 Themeing Updates including Nicer Code Formatting 2022-04-12 14:15:58 -07:00
Dan Ballard 47795094a0 Merge pull request 'Add Hook into Add Contact Flow to better Gauge Intent' (#424) from add_contact_hook into trunk
Reviewed-on: cwtch.im/cwtch-ui#424
2022-04-12 19:27:18 +00:00
Sarah Jamie Lewis 0d1e7bb5a0 Add Hook into Add Contact Flow to better Gauge Intent
(This the future we can expand this, use this information to better guide people)
2022-04-12 12:15:39 -07:00
Sarah Jamie Lewis 987b80c92b Merge pull request 'Message Formatting Experiment Initial Commit' (#413) from message-formatting into trunk
Reviewed-on: cwtch.im/cwtch-ui#413
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-06 22:15:44 +00:00
Sarah Jamie Lewis e718adad8a Add Roboto Fonts License 2022-04-06 15:04:17 -07:00
Sarah Jamie Lewis 0b9c159e85 Icon and Log Error 2022-04-06 14:54:53 -07:00
Sarah Jamie Lewis a4a2af08b4 Message Formatting Experiment Initial Commit 2022-04-06 14:36:34 -07:00
Sarah Jamie Lewis 471a729d46 Merge pull request 'port most gomobile FlwtchWorker calls to lcg to MainActivity; sendmessage sets lastSeen time' (#412) from mainActivityPort into trunk
Reviewed-on: cwtch.im/cwtch-ui#412
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-06 19:01:40 +00:00
Dan Ballard 1cffea5c1a port most gomobile FlwtchWorker calls to lcg to MainActivity; sendmessage sets lastSeen time 2022-04-05 18:38:59 -07:00
Sarah Jamie Lewis e7c5b2cfa5 Merge pull request 'store last seen time in lcg and handle unread counts' (#411) from unreadSync into trunk
Reviewed-on: cwtch.im/cwtch-ui#411
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-04 22:17:59 +00:00
Dan Ballard e08114881c store last seen time in lcg and handle unread counts 2022-04-04 15:02:37 -07:00
Sarah Jamie Lewis 6eaf95a33b Merge pull request 'only send message on Enter Up event' (#408) from androMessage into trunk
Reviewed-on: cwtch.im/cwtch-ui#408
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-03-24 23:39:48 +00:00
Dan Ballard 0db68bcdbb Merge branch 'trunk' into androMessage 2022-03-24 23:37:31 +00:00
Dan Ballard f64559191b only send message on Enter Up event 2022-03-24 16:25:08 -07:00
Dan Ballard b8c1c7682b Merge pull request 'message cache allows index locking, rework messageHandler to use bulk fetching, sendMessage flow with no sleep; move some core getMessages/SendMessage handlers from FlwtchWorker to MainActivity' (#407) from androMessage into trunk
Reviewed-on: cwtch.im/cwtch-ui#407
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-03-24 19:41:16 +00:00
Dan Ballard 9812111041 comments, organizing logic 2022-03-24 12:04:09 -07:00
Dan Ballard ecc9a3a48c comments, fix new messages marker logic 2022-03-23 18:02:26 -07:00
Dan Ballard 523531e6be new lcg version 2022-03-23 18:02:26 -07:00
Dan Ballard ff3e60a750 message cache allows index locking, rework messageHandler to use bulk fetching, sendMessage flow with no sleep; move some core getMessages/SendMessage handlers from FlwtchWorker to MainActivity 2022-03-23 18:01:43 -07:00
Dan Ballard 5a1c66bc25 Merge pull request 'Renable Network Status Updates, Display Status in Contacts View' (#406) from ns into trunk
Reviewed-on: cwtch.im/cwtch-ui#406
2022-03-23 21:42:28 +00:00
Sarah Jamie Lewis 10780ac8cb Merge branch 'trunk' into ns 2022-03-23 21:24:14 +00:00
Sarah Jamie Lewis 0857d46809 Renable Network Status Updates, Display Status in Contacts View 2022-03-23 14:23:23 -07:00
Dan Ballard d7d3b2ef97 Merge pull request 'Fix: #344 - Folder Selection on Settings can be Opened Multiple times' (#402) from small_edits into trunk
Reviewed-on: cwtch.im/cwtch-ui#402
2022-03-22 02:17:14 +00:00
Sarah Jamie Lewis 65d5e9777d Fix: 344 - Folder Selection on Settings can be Opened Multiple times 2022-03-21 10:49:53 -07:00
Dan Ballard 27f4c5f00e Merge pull request 'Localizations + Allow Editing when Contact is Offline' (#400) from small_edits into trunk
Reviewed-on: cwtch.im/cwtch-ui#400
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-03-21 16:37:26 +00:00
Sarah Jamie Lewis f48b6af3dd Localizations + Allow Editing 2022-03-21 09:23:16 -07:00
Dan Ballard d8e19de5b1 Merge pull request 'Import and Export Profile' (#397) from import_export into trunk
Reviewed-on: cwtch.im/cwtch-ui#397
2022-03-11 21:10:03 +00:00
Sarah Jamie Lewis af03dd30cc Upgrade libcwtch 2022-03-11 12:27:44 -08:00
Sarah Jamie Lewis 8a3867b5b3 Import / Export for Android 2022-03-11 12:26:23 -08:00
Sarah Jamie Lewis 6237032716 Import and Export Profile 2022-03-11 12:26:21 -08:00
Dan Ballard 915cf1a6d8 Merge pull request 'splash on shutdown; android stability: check if lcg started' (#399) from splashExit into trunk
Reviewed-on: cwtch.im/cwtch-ui#399
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-03-11 00:45:45 +00:00
Dan Ballard c4ebed0a71 splash on shutdown; android stability: check if lcg started 2022-03-10 16:45:18 -08:00
Dan Ballard 3c71bb8184 Merge pull request 'Using build context instead of inherited context when building file bubble popout' (#395) from file_bubble_pop_fix into trunk
Reviewed-on: cwtch.im/cwtch-ui#395
2022-03-08 19:08:12 +00:00
Sarah Jamie Lewis c3661d4caa Using build context instead of inherited context when building file bubble popout
Fix: #394
2022-03-08 10:56:20 -08:00
Sarah Jamie Lewis 62a99797ca Merge pull request 'assorted android and sync progress fixes' (#391) from state into trunk
Reviewed-on: cwtch.im/cwtch-ui#391
2022-03-04 21:17:41 +00:00
Dan Ballard 7cfa9432c8 unbreak notifications on android ([Pp]ictures) + asset dup; fix sync progres resume logic 2022-03-04 13:15:08 -08:00
Dan Ballard 1d0cb785c1 fix android segfault in flwtch worker; try/catch to catch future bugs in flwtch worker; resume servers load sync status from cwtch; add bg color to sync progress bar; showdown now synchronous so completes 2022-03-04 13:15:08 -08:00
Dan Ballard 8eaa3974c9 Merge pull request 'Stop using key-based lookups for messages, use the message cache instead.' (#392) from message_row_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#392
2022-03-04 21:14:34 +00:00
Sarah Jamie Lewis 6cc5146744 Readd-widget key cache 2022-03-04 12:14:43 -08:00
Sarah Jamie Lewis 1fea540f9d Stop using key-based lookups for messages, use the message cache instead.
Always update alignment to allow for message row objects to be reused
2022-03-04 12:14:43 -08:00
Dan Ballard 7457246a01 Merge pull request 'Finish up Danish translation' (#390) from da into trunk
Reviewed-on: cwtch.im/cwtch-ui#390
2022-03-03 18:52:02 +00:00
Sarah Jamie Lewis 0a26a1899b Finidh up Danish translation 2022-03-03 10:19:42 -08:00
Sarah Jamie Lewis 8183fbd987 Merge pull request 'danish-localization' (#388) from christofal/cwtch-ui:danish-localization into trunk
Reviewed-on: cwtch.im/cwtch-ui#388
2022-03-03 17:57:12 +00:00
Sarah Jamie Lewis f3f5f65e22 Merge branch 'trunk' into danish-localization 2022-03-03 17:56:26 +00:00
Dan Ballard c565089578 Merge pull request 'Spanish and Welsh complete' (#387) from es into trunk
Reviewed-on: cwtch.im/cwtch-ui#387
2022-03-02 23:04:32 +00:00
Sarah Jamie Lewis 009f99e0f5 Merge branch 'trunk' into es 2022-03-02 23:01:35 +00:00
Sarah Jamie Lewis 0894fc577b Spanish and Welsh complete 2022-03-02 15:00:34 -08:00
Dan Ballard b0977b31a5 Merge pull request 'Support Welsh, Update Translations for other Languages' (#385) from cy into trunk
Reviewed-on: cwtch.im/cwtch-ui#385
2022-03-02 19:31:46 +00:00
Sarah Jamie Lewis 6df922d64e Merge branch 'trunk' into cy 2022-03-02 19:29:54 +00:00
Sarah Jamie Lewis b70de4052d Support Welsh, Update Translations for other Languages 2022-03-02 11:28:43 -08:00
Allan Christoffersen 453558f034 Full Danish localization 2022-03-02 15:32:47 +01:00
Allan Christoffersen 481890b55f Initial commit of Danish localization 2022-03-02 14:30:53 +01:00
Dan Ballard 7122db0388 Merge pull request 'Move messageDate format handling to MessageBubbleDecoration' (#384) from messagedate into trunk
Reviewed-on: cwtch.im/cwtch-ui#384
2022-03-01 17:39:23 +00:00
Sarah Jamie Lewis c56f40c090 Merge branch 'trunk' into messagedate 2022-03-01 06:36:51 +00:00
Sarah Jamie Lewis c4c693144d Move messageDate format handling to MessageBubbleDecoration 2022-02-28 22:35:09 -08:00
Dan Ballard 891bf51a70 Merge pull request 'harmonize translations from lokalize' (#383) from i18n into trunk
Reviewed-on: cwtch.im/cwtch-ui#383
2022-03-01 00:02:33 +00:00
Sarah Jamie Lewis a559b0caf8 Update No with clobbered 2022-02-28 16:01:10 -08:00
Sarah Jamie Lewis 054e5fca84 harmonize translations from lokalize 2022-02-28 15:43:33 -08:00
Dan Ballard 6b5f4febe7 Merge pull request 'Full Norwegian Translation' (#382) from no into trunk
Reviewed-on: cwtch.im/cwtch-ui#382
2022-02-28 23:39:23 +00:00
Sarah Jamie Lewis 2c55f78913 Small updates 2022-02-28 15:19:25 -08:00
Henrik Austad f1cfd2c30f Norwegian localization 2022-02-28 15:18:39 -08:00
Sarah Jamie Lewis b36e76b818 remove norwya to add better 2022-02-28 15:18:21 -08:00
Dan Ballard 2aadea0cea Merge pull request 'Complete German Translation.' (#381) from de into trunk
Reviewed-on: cwtch.im/cwtch-ui#381
2022-02-28 20:49:44 +00:00
Dan Ballard 423a2bce5e Merge branch 'trunk' into de 2022-02-28 20:49:23 +00:00
Dan Ballard eef40f76f9 Merge pull request 'Add support for many prospective new languages' (#380) from lb into trunk
Reviewed-on: cwtch.im/cwtch-ui#380
2022-02-28 20:49:04 +00:00
Sarah Jamie Lewis 385f86be02 Complete German Translation 2022-02-28 12:29:20 -08:00
Sarah Jamie Lewis 193a9d6f89 Add support for many prospective new languages 2022-02-28 12:05:22 -08:00
Dan Ballard 2ade7e8e4f Merge pull request 'Initial support for Romanian localization' (#379) from ro into trunk
Reviewed-on: cwtch.im/cwtch-ui#379
2022-02-27 20:04:50 +00:00
Sarah Jamie Lewis 12a0fc1059 Update French 2022-02-27 12:02:17 -08:00
Sarah Jamie Lewis 82542664ad Merge branch 'trunk' into ro 2022-02-27 19:37:49 +00:00
Sarah Jamie Lewis 670d8bc343 Initial support for Romanian localization 2022-02-27 11:36:51 -08:00
Dan Ballard ce1db17148 Merge pull request 'WIP make NixNotificationManager using flutter_local_notification' (#375) from macNotifications into trunk
Reviewed-on: cwtch.im/cwtch-ui#375
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-02-24 00:22:06 +00:00
Dan Ballard 018a51b76e Merge branch 'macNotifications' of git.openprivacy.ca:cwtch.im/cwtch-ui into macNotifications 2022-02-23 16:21:50 -08:00
Dan Ballard 61cdb37226 lcg bump 2022-02-23 16:21:42 -08:00
Dan Ballard 5b4778dd78 Merge branch 'trunk' into macNotifications 2022-02-23 19:33:29 +00:00
Dan Ballard 152f5fbc96 garuntee no notifications while using app for nix 2022-02-23 11:27:15 -08:00
Dan Ballard 5e7272b15a nix notification manager only use title, body is unsafe 2022-02-23 11:13:03 -08:00
Dan Ballard 9473acd438 Merge pull request 'Add Message Status Widget to File Bubble' (#376) from fix297 into trunk
Reviewed-on: cwtch.im/cwtch-ui#376
2022-02-22 20:49:34 +00:00
Sarah Jamie Lewis 4fd8075497 Merge branch 'trunk' into fix297 2022-02-22 19:14:25 +00:00
Sarah Jamie Lewis 70eb160abc Add Message Status Widget to File Bubble
Also fix bug in peer settings
2022-02-22 11:09:44 -08:00
Dan Ballard 1a4dccf44a disable sound until we make it a setting 2022-02-20 09:53:15 -08:00
Dan Ballard 7509c20a62 make NixNotificationManager using flutter_local_notification 2022-02-18 15:50:34 -08:00
Dan Ballard 68c2e1547a Merge pull request 'Check WorkInfo is not Null' (#374) from cwtch1.6.1-fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#374
2022-02-16 23:58:45 +00:00
Sarah Jamie Lewis 705b6e02c9 Fix overlap in debug settings 2022-02-16 13:49:29 -08:00
Sarah Jamie Lewis 137de57e83 Check WorkInfo is Null
This shouldn't happen in normal use, but can happen in debug builds
2022-02-16 13:44:32 -08:00
Dan Ballard 6859780873 Merge pull request 'cwtch1.6.1-fixes' (#373) from cwtch1.6.1-fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#373
2022-02-16 18:34:44 +00:00
Sarah Jamie Lewis dab09c6acb Fix #314 - Numpad enter should send message 2022-02-14 12:20:25 -08:00
Sarah Jamie Lewis 7bf2e15009 Formatting 2022-02-14 11:03:28 -08:00
Sarah Jamie Lewis a0f8be2d53 Bump Android Version 2022-02-14 11:02:19 -08:00
Sarah Jamie Lewis 02407c5abe Fix #371
Fix #368
Fix #16
2022-02-14 11:01:58 -08:00
Sarah Jamie Lewis 635e383f65 Merge pull request 'pubspec version bump for android' (#369) from ps160 into trunk
Reviewed-on: cwtch.im/cwtch-ui#369
2022-02-11 18:59:16 +00:00
Dan Ballard 1ec9be3d9a pubspec version bump for android 2022-02-11 13:57:25 -05:00
Sarah Jamie Lewis fd886e7315 Merge pull request 'libcwtch go 1.6.0' (#367) from lcg160 into trunk
Reviewed-on: cwtch.im/cwtch-ui#367
2022-02-10 21:18:14 +00:00
Dan Ballard 387816ea0f libcwtch go 1.6.0 2022-02-10 15:58:09 -05:00
Dan Ballard 3cb6c9d9f4 Merge pull request 'Allow cwtch: prefix' (#366) from cwtch_prefix into trunk
Reviewed-on: cwtch.im/cwtch-ui#366
2022-02-09 22:37:27 +00:00
Sarah Jamie Lewis f1688c5f8f upgrade lcg 2022-02-09 14:30:41 -08:00
Sarah Jamie Lewis d5296d2211 Merge branch 'trunk' into cwtch_prefix 2022-02-09 21:59:31 +00:00
Sarah Jamie Lewis 953971980f Allow cwtch: prefix 2022-02-09 13:58:40 -08:00
Sarah Jamie Lewis 7e59d1a526 Merge pull request 'mac lcg' (#360) from maclcg into trunk
Reviewed-on: cwtch.im/cwtch-ui#360
2022-02-09 02:42:28 +00:00
Dan Ballard 783d666486 mac lcg 2022-02-08 21:41:19 -05:00
Dan Ballard 040ba80480 Merge pull request 'notificationSettings' (#354) from notificationSettings into trunk
Reviewed-on: cwtch.im/cwtch-ui#354
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-02-08 23:21:28 +00:00
Dan Ballard 8ba54469eb comment/format fix 2022-02-08 18:21:06 -05:00
Dan Ballard 706d1da518 new lcg version 2022-02-08 17:50:32 -05:00
Dan Ballard b5511ae723 Merge branch 'trunk' into notificationSettings 2022-02-08 17:11:29 -05:00
Dan Ballard 4c47198977 notification policy work, translations, even for notifications 2022-02-08 17:07:39 -05:00
erinn 9a17852533 Merge pull request 'Add Profile Image Preview to Peer Settings + other UI Profile Image Fixups' (#359) from custom_profile_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#359
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-08 22:07:32 +00:00
Sarah Jamie Lewis 2a07ba8ed7 revert message row image size change 2022-02-08 14:05:23 -08:00
Sarah Jamie Lewis 2e5ee796fa Add Profile Image Preview to Peer Settings + other UI Profile Image Fixups 2022-02-08 14:05:09 -08:00
Dan Ballard d1d3f23f82 android notification policy/content 2022-02-08 12:35:25 -05:00
Dan Ballard fa6e399aab android work 2022-02-08 11:44:00 -05:00
Dan Ballard ddefcb8ff2 rejig notification policy globally and conversationally 2022-02-08 11:40:15 -05:00
Dan Ballard b382c3d349 desktop support for notification settings 2022-02-08 11:34:05 -05:00
Dan Ballard c550437aa5 settings pane headers, and notification settings options 2022-02-08 11:34:05 -05:00
erinn e6246cf44a Merge pull request 'Update Goldens' (#358) from custom_profile_images into trunk
Reviewed-on: cwtch.im/cwtch-ui#358
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-07 23:38:50 +00:00
Sarah Jamie Lewis d71574a831 Merge branch 'trunk' into custom_profile_images 2022-02-07 23:35:43 +00:00
Sarah Jamie Lewis 62bca86c19 Update Goldens 2022-02-07 15:35:01 -08:00
erinn 729ff6811e Merge pull request 'Profile Images' (#355) from custom_profile_images into trunk
Reviewed-on: cwtch.im/cwtch-ui#355
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-07 23:16:46 +00:00
Sarah Jamie Lewis bf4cfde7df Fixup Context Listen 2022-02-07 15:16:02 -08:00
Sarah Jamie Lewis 403454d6b8 Add Edit Badge 2022-02-07 15:12:36 -08:00
Sarah Jamie Lewis d902ba5cce Rename Constant 2022-02-07 14:59:09 -08:00
Sarah Jamie Lewis 5b5fe586e8 Update Lib Cwtch, Allow Deleting P2P contacts, Formatting 2022-02-07 14:53:33 -08:00
Sarah Jamie Lewis b280765631 Fallback to Default Profile Images when Image Previews are Disabled 2022-02-07 14:26:14 -08:00
Sarah Jamie Lewis 2a2d808b60 Disable image previews when file sharing is disables 2022-02-07 12:23:26 -08:00
Sarah Jamie Lewis d158d7d619 Select Profile Image tooltip + restrict selection only when image previews are enabled 2022-02-07 12:20:54 -08:00
Sarah Jamie Lewis c6192ef736 Factor out showFilePicker into a generic controller 2022-02-07 11:30:17 -08:00
Sarah Jamie Lewis 3d85883f8e Profile Images 2022-02-04 16:57:31 -08:00
erinn e22db92dc1 add gherkin tests (#353)
* not all tests are complete
* language and theme tests fail due to upstream issue with dropdown boxes
* not yet autorun or reported by drone

Co-authored-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
Reviewed-on: cwtch.im/cwtch-ui#353
Co-authored-by: erinn <erinn@openprivacy.ca>
Co-committed-by: erinn <erinn@openprivacy.ca>
2022-02-05 00:37:25 +00:00
170 changed files with 346046 additions and 2070 deletions

View File

@ -1 +1 @@
2022-01-26-15-10-v1.5.4-18-gd77d7bb
2022-04-21-19-14-1.7.1

View File

@ -1 +1 @@
2022-01-26-20-10-v1.5.4-18-gd77d7bb
2022-04-21-23-14-1.7.1

View File

@ -6,7 +6,7 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:name="${applicationName}"
android:label="Cwtch"
android:extractNativeLibs="true"
android:icon="@mipmap/knott">
@ -46,7 +46,15 @@
<!--Needed to run in background (lol)-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<!-- Ability to ask user to exempt app from power management (which can kill it more frequently especially on some devices.
Allows app to use ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -->
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- TODO when we support sdk 31
<uses-permission-sdk-23 android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
-->
<!--Needed to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
<queries>

View File

@ -0,0 +1,20 @@
// Generated file.
// If you wish to remove Flutter's multidex support, delete this entire file.
package io.flutter.app;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
*/
public class FlutterMultiDexApplication extends FlutterApplication {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

View File

@ -1,6 +1,7 @@
package im.cwtch.flwtch
import android.app.*
import android.os.Environment
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
@ -29,13 +30,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
private var notificationID: MutableMap<String, Int> = mutableMapOf()
private var notificationIDnext: Int = 1
private var notificationSimple: String? = null
private var notificationConversationInfo: String? = null
private val TAG: String = "FlwtchWorker.kt"
override suspend fun doWork(): Result {
// Hack to uncomment and deploy if your device has zombie workers you need to kill
// We need a proper solution but this will clear those out for now
/*if (notificationSimple == null) {
Log.e("FlwtchWorker", "doWork found notificationSimple is null, app has not started, this is a stale thread, terminating")
return Result.failure()
}*/
val method = inputData.getString(KEY_METHOD)
?: return Result.failure()
val args = inputData.getString(KEY_ARGS)
?: return Result.failure()
// Mark the Worker as important
val progress = "Cwtch is keeping Tor running in the background"//todo:translate
val progress = "Cwtch is keeping Tor running in the background" // TODO: translate
setForeground(createForegroundInfo(progress))
return handleCwtch(method, args)
}
@ -49,334 +63,195 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
}
private fun handleCwtch(method: String, args: String): Result {
val a = JSONObject(args)
when (method) {
"Start" -> {
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
val appDir = (a.get("appDir") as? String) ?: ""
val torPath = (a.get("torPath") as? String) ?: "tor"
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
if (method != "Start") {
if (Cwtch.started() != 1.toLong()) {
Log.e(TAG, "libCwtch-go reports it is not initialized yet")
return Result.failure()
}
}
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
val a = JSONObject(args)
when (method) {
"Start" -> {
Log.i(TAG, "handleAppInfo Start")
val appDir = (a.get("appDir") as? String) ?: ""
val torPath = (a.get("torPath") as? String) ?: "tor"
Log.i(TAG, "appDir: '$appDir' torPath: '$torPath'")
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
val downloadIDs = mutableMapOf<String, Int>()
while(true) {
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
val data = JSONObject(evt.Data)
val handle = if (evt.EventType == "NewMessageFromPeer") data.getString("RemotePeer") else data.getString("GroupID");
if (data["RemotePeer"] != data["ProfileOnion"]) {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel(handle, handle)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
val downloadIDs = mutableMapOf<String, Int>()
while (true) {
try {
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
// TODO replace this notification block with the NixNotification manager in dart as it has access to contact names and also needs less working around
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
val data = JSONObject(evt.Data)
val handle = data.getString("RemotePeer");
val conversationId = data.getInt("ConversationID").toString();
val notificationChannel = if (evt.EventType == "NewMessageFromPeer") handle else conversationId
if (data["RemotePeer"] != data["ProfileOnion"]) {
val notification = data["notification"]
if (notification == "SimpleEvent") {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel("Cwtch", "Cwtch")
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle("Cwtch")
.setContentText(notificationSimple ?: "New Message")
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID("Cwtch", "Cwtch"), newNotification)
} else if (notification == "ContactInfo") {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel(notificationChannel, notificationChannel)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val loader = FlutterInjector.instance().flutterLoader()
Log.i(TAG, "notification for " + evt.EventType + " " + handle + " " + conversationId + " " + channelId)
Log.i(TAG, data.toString());
val key = loader.getLookupKeyForAsset(data.getString("picture"))//"assets/profiles/001-centaur.png")
val fh = applicationContext.assets.open(key)
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
intent.putExtra("Handle", handle)
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(data.getString("Nick"))
.setContentText((notificationConversationInfo
?: "New Message From %1").replace("%1", data.getString("Nick")))
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), channelId), newNotification)
}
val loader = FlutterInjector.instance().flutterLoader()
val key = loader.getLookupKeyForAsset("assets/" + data.getString("Picture"))//"assets/profiles/001-centaur.png")
val fh = applicationContext.assets.open(key)
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
intent.putExtra("Handle", handle)
}
} else if (evt.EventType == "FileDownloadProgressUpdate") {
try {
val data = JSONObject(evt.Data);
val fileKey = data.getString("FileKey");
val title = data.getString("NameSuggestion");
val progress = data.getString("Progress").toInt();
val progressMax = data.getString("FileSizeInChunks").toInt();
if (!downloadIDs.containsKey(fileKey)) {
downloadIDs.put(fileKey, downloadIDs.count());
}
var dlID = downloadIDs.get(fileKey);
if (dlID == null) {
dlID = 0;
}
if (progress >= 0) {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createDownloadNotificationChannel(fileKey, fileKey)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
};
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setOngoing(true)
.setContentTitle("Downloading")//todo: translate
.setContentText(title)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setProgress(progressMax, progress, false)
.setSound(null)
//.setSilent(true)
.build();
notificationManager.notify(dlID, newNotification);
}
} catch (e: Exception) {
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
}
} else if (evt.EventType == "FileDownloaded") {
Log.d(TAG, "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey");
if (tempFile != "" && tempFile != data.getString("FilePath")) {
val filePath = data.getString("FilePath");
Log.i(TAG, "moving " + tempFile + " to " + filePath);
val sourcePath = Paths.get(tempFile);
val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("TAG", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
Files.delete(sourcePath);
}
}
if (downloadIDs.containsKey(fileKey)) {
notificationManager.cancel(downloadIDs.get(fileKey) ?: 0);
}
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(data.getString("Nick"))
.setContentText("New message")//todo: translate
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
}
} else if (evt.EventType == "FileDownloadProgressUpdate") {
try {
val data = JSONObject(evt.Data);
val fileKey = data.getString("FileKey");
val title = data.getString("NameSuggestion");
val progress = data.getString("Progress").toInt();
val progressMax = data.getString("FileSizeInChunks").toInt();
if (!downloadIDs.containsKey(fileKey)) {
downloadIDs.put(fileKey, downloadIDs.count());
Intent().also { intent ->
intent.action = "im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS"
intent.putExtra("EventType", evt.EventType)
intent.putExtra("Data", evt.Data)
intent.putExtra("EventID", evt.EventID)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}
var dlID = downloadIDs.get(fileKey);
if (dlID == null) {
dlID = 0;
}
if (progress >= 0) {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createDownloadNotificationChannel(fileKey, fileKey)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
};
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setOngoing(true)
.setContentTitle("Downloading")//todo: translate
.setContentText(title)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setProgress(progressMax, progress, false)
.setSound(null)
//.setSilent(true)
.build();
notificationManager.notify(dlID, newNotification);
if (evt.EventType == "Shutdown") {
Log.i(TAG, "processing shutdown event, exiting FlwtchWorker/Start()...");
return Result.success()
}
} catch (e: Exception) {
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
}
} else if (evt.EventType == "FileDownloaded") {
Log.d("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey");
if (tempFile != "" && tempFile != data.getString("FilePath")) {
val filePath = data.getString("FilePath");
Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath);
val sourcePath = Paths.get(tempFile);
val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
Files.delete(sourcePath);
}
}
if (downloadIDs.containsKey(fileKey)) {
notificationManager.cancel(downloadIDs.get(fileKey)?:0);
Log.e(TAG, "Error in handleCwtch: " + e.toString() + " :: " + e.getStackTrace());
}
}
Intent().also { intent ->
intent.action = "im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS"
intent.putExtra("EventType", evt.EventType)
intent.putExtra("Data", evt.Data)
intent.putExtra("EventID", evt.EventID)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}
}
// Event passing translations from Flutter to Kotlin worker scope so the worker can use them
"L10nInit" -> {
notificationSimple = (a.get("notificationSimple") as? String) ?: "New Message"
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
?: "New Message From "
}
else -> {
Log.i(TAG, "unknown command: " + method);
return Result.failure()
}
}
"ReconnectCwtchForeground" -> {
Cwtch.reconnectCwtchForeground()
}
"CreateProfile" -> {
val nick = (a.get("nick") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass = (a.get("pass") as? String) ?: ""
Cwtch.loadProfiles(pass)
}
"ChangePassword" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val pass = (a.get("OldPass") as? String) ?: ""
val passNew = (a.get("NewPass") as? String) ?: ""
val passNew2 = (a.get("NewPassAgain") as? String) ?: ""
Cwtch.changePassword(profile, pass, passNew, passNew2)
}
"GetMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val indexI = a.getInt("index").toLong()
Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
}
"GetMessageByID" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val id = a.getInt("id").toLong()
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build())
}
"GetMessageByContentHash" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build())
}
"UpdateMessageAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val channel = a.getInt("chanenl").toLong()
val midx = a.getInt("midx").toLong()
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value)
}
"AcceptConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.acceptConversation(profile, conversation)
}
"BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.blockContact(profile, conversation)
}
"UnblockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.unblockContact(profile, conversation)
}
"SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, conversation, message)
}
"SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val target = a.getInt("target").toLong()
Cwtch.sendInvitation(profile, conversation, target)
}
"ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, conversation, filepath)
}
"DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: ""
val manifestpath = (a.get("manifestpath") as? String) ?: ""
val filekey = (a.get("filekey") as? String) ?: ""
// FIXME: Prevent spurious calls by Intent
if (profile != "") {
Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
}
}
"CheckDownloadStatus" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.checkDownloadStatus(profile, fileKey)
}
"VerifyOrResumeDownload" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendProfileEvent(onion, jsonEvent)
}
"SendAppEvent" -> {
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendAppEvent(jsonEvent)
}
"ResetTor" -> {
Cwtch.resetTor()
}
"ImportBundle" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle)
}
"CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: ""
val groupName = (a.get("groupName") as? String) ?: ""
Cwtch.createGroup(profile, server, groupName)
}
"DeleteProfile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.deleteProfile(profile, pass)
}
"ArchiveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.archiveConversation(profile, conversation)
}
"DeleteConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.deleteContact(profile, conversation)
}
"SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v)
}
"SetConversationAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setConversationAttribute(profile, conversation, key, v)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
"LoadServers" -> {
val password = (a.get("Password") as? String) ?: ""
Cwtch.loadServers(password)
}
"CreateServer" -> {
val password = (a.get("Password") as? String) ?: ""
val desc = (a.get("Description") as? String) ?: ""
val autostart = (a.get("Autostart") as? Boolean) ?: false
Cwtch.createServer(password, desc, autostart)
}
"DeleteServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val password = (a.get("Password") as? String) ?: ""
Cwtch.deleteServer(serverOnion, password)
}
"LaunchServers" -> {
Cwtch.launchServers()
}
"LaunchServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.launchServer(serverOnion)
}
"StopServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.stopServer(serverOnion)
}
"StopServers" -> {
Cwtch.stopServers()
}
"DestroyServers" -> {
Cwtch.destroyServers()
}
"SetServerAttribute" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setServerAttribute(serverOnion, key, v)
}
else -> {
Log.i("FlwtchWorker", "unknown command: " + method);
return Result.failure()
}
}
return Result.success()
return Result.success()
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
val id = "flwtch"
val title = "Flwtch"
val cancel = "Shut down"//todo: translate
val title = "Flwtch" // TODO: change
val cancel = "Shut down" // TODO: translate
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundNotificationChannel(id, id)

View File

@ -1,46 +1,48 @@
package im.cwtch.flwtch
import SplashView
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.annotation.NonNull
import android.content.pm.PackageManager
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.util.Log
import android.view.Window
import android.view.WindowManager
import androidx.annotation.NonNull
import androidx.lifecycle.Observer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import io.flutter.embedding.android.SplashScreen
import cwtch.Cwtch
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.ErrorLogResult
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONObject
import java.util.concurrent.TimeUnit
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.concurrent.TimeUnit
import android.net.Uri
import android.provider.DocumentsContract
import android.content.ContentUris
import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
// Channel to get app info
private val CHANNEL_APP_INFO = "test.flutter.dev/applicationInfo"
private val CALL_APP_INFO = "getNativeLibDir"
private val ANDROID_SETTINGS_CHANNEL_NAME = "androidSettings"
private val ANDROID_SETTINGS_CHANGE_NAME= "androidSettingsChanged"
private var andoidSettingsChangeChannel: MethodChannel? = null
private val CALL_ASK_BATTERY_EXEMPTION = "requestBatteryExemption"
private val CALL_IS_BATTERY_EXEMPT = "isBatteryExempt"
// Channel to get cwtch api calls on
private val CHANNEL_CWTCH = "cwtch"
@ -52,6 +54,7 @@ class MainActivity: FlutterActivity() {
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
private val CHANNEL_SHUTDOWN_CLICK = "im.cwtch.flwtch/shutdownClickHandler"
private val TAG: String = "MainActivity.kt"
// WorkManager tag applied to all Start() infinite coroutines
val WORKER_TAG = "cwtchEventBusWorker"
@ -62,11 +65,27 @@ class MainActivity: FlutterActivity() {
// "Download to..." prompt extra arguments
private val FILEPICKER_REQUEST_CODE = 234
private val PREVIEW_EXPORT_REQUEST_CODE = 235
private val PROFILE_EXPORT_REQUEST_CODE = 236
private val REQUEST_DOZE_WHITELISTING_CODE:Int = 9
private var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
private var exportFromPath = ""
override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
// Todo: when we support SDK 31
// hideOverlay()
}
/*
@TargetApi(31)
fun hideOverlay() {
window.setHideOverlayWindows(true);
}
*/
// handles clicks received from outside the app (ie, notifications)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@ -93,8 +112,16 @@ class MainActivity: FlutterActivity() {
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
super.onActivityResult(requestCode, result, intent);
// has null intent and data
if (requestCode == REQUEST_DOZE_WHITELISTING_CODE) {
// 0 == "battery optimized" (still)
// -1 == "no battery optimization" (exempt!)
andoidSettingsChangeChannel!!.invokeMethod("powerExemptionChange", result == -1)
return;
}
if (intent == null || intent!!.getData() == null) {
Log.i("MainActivity:onActivityResult", "user canceled activity");
Log.i(TAG, "user canceled activity");
return;
}
@ -110,8 +137,6 @@ class MainActivity: FlutterActivity() {
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
} else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) {
val targetPath = intent!!.getData().toString()
var srcFile = File(this.exportFromPath)
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file")
val sourcePath = Paths.get(this.exportFromPath);
val targetUri = Uri.parse(targetPath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
@ -122,6 +147,20 @@ class MainActivity: FlutterActivity() {
os?.close();
//Files.delete(sourcePath);
}
} else if (requestCode == PROFILE_EXPORT_REQUEST_CODE ) {
val targetPath = intent!!.getData().toString()
val srcFile = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.exportFromPath).toString();
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file " + srcFile);
val sourcePath = Paths.get(srcFile);
val targetUri = Uri.parse(targetPath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
//Files.delete(sourcePath);
}
}
}
@ -132,10 +171,13 @@ class MainActivity: FlutterActivity() {
requestWindowFeature(Window.FEATURE_NO_TITLE)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANNEL_NAME).setMethodCallHandler { call, result -> handleAndroidSettings(call, result) }
notificationClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
shutdownClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_SHUTDOWN_CLICK)
andoidSettingsChangeChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANGE_NAME)
}
// MethodChannel CHANNEL_APP_INFO handler (Flutter Channel for requests for Android environment info)
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
CALL_APP_INFO -> result.success(getNativeLibDir())
@ -144,6 +186,30 @@ class MainActivity: FlutterActivity() {
}
}
// MethodChannel ANDROID_SETTINGS_CHANNEL_NAME handler (Flutter Channel for requests for Android settings)
// Called from lib/view/globalsettingsview.dart
private fun handleAndroidSettings(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
CALL_IS_BATTERY_EXEMPT -> result.success(checkIgnoreBatteryOpt() ?: false);
CALL_ASK_BATTERY_EXEMPTION -> { requestBatteryExemption(); result.success(null); }
else -> result.notImplemented()
}
}
@TargetApi(23)
private fun checkIgnoreBatteryOpt(): Boolean {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(this.packageName) ?: false;
}
@TargetApi(23)
private fun requestBatteryExemption() {
val i = Intent()
i.action = ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
i.data = Uri.parse("package:" + this.packageName)
startActivityForResult(i, REQUEST_DOZE_WHITELISTING_CODE);
}
private fun getNativeLibDir(): String {
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
"im.cwtch.flwtch", // Must be app name
@ -154,76 +220,314 @@ class MainActivity: FlutterActivity() {
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
var method = call.method
// todo change usage patern to match that in FlwtchWorker
// Unsafe for anything using int args, causes access time attempt to cast to string which will fail
val argmap: Map<String, String> = call.arguments as Map<String, String>
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
// the eventbus listener.
if (call.method == "Start") {
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
when (call.method) {
"Start" -> {
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
// accidentally duplicated. however, we still need to manually check if it's running or not, so
// that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) {
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
WorkManager.getInstance(this).pruneWork()
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
// accidentally duplicated. however, we still need to manually check if it's running or not, so
// that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) {
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
WorkManager.getInstance(this).pruneWork()
Log.i("MainActivity.kt", "Start() launching foregroundservice")
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
return
} else if (call.method == "CreateDownloadableFile") {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
Log.i("MainActivity.kt", "Start() launching foregroundservice")
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
}
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
return
} else if (call.method == "ExportPreviewedFile") {
this.exportFromPath = argmap["Path"] ?: ""
val suggestion = argmap["FileName"] ?: "filename.ext"
var imgType = "jpeg"
if (suggestion.endsWith("png")) {
imgType = "png"
} else if (suggestion.endsWith("webp")) {
imgType = "webp"
} else if (suggestion.endsWith("bmp")) {
imgType = "bmp"
} else if (suggestion.endsWith("gif")) {
imgType = "gif"
"CreateDownloadableFile" -> {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/" + imgType
putExtra(Intent.EXTRA_TITLE, suggestion)
"ExportPreviewedFile" -> {
this.exportFromPath = argmap["Path"] ?: ""
val suggestion = argmap["FileName"] ?: "filename.ext"
var imgType = "jpeg"
if (suggestion.endsWith("png")) {
imgType = "png"
} else if (suggestion.endsWith("webp")) {
imgType = "webp"
} else if (suggestion.endsWith("bmp")) {
imgType = "bmp"
} else if (suggestion.endsWith("gif")) {
imgType = "gif"
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/" + imgType
putExtra(Intent.EXTRA_TITLE, suggestion)
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
return
}
"ExportProfile" -> {
this.exportFromPath = argmap["file"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/gzip"
putExtra(Intent.EXTRA_TITLE, argmap["file"])
}
startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
}
"GetMessages" -> {
Log.d("MainActivity.kt", "Cwtch GetMessages")
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
val profile = argmap["ProfileOnion"] ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val indexI: Int = call.argument("index") ?: 0
val count: Int = call.argument("count") ?: 1
result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong()))
return
}
"SendMessage" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val message: String = call.argument("message") ?: ""
result.success(Cwtch.sendMessage(profile, conversation.toLong(), message))
return
}
"SendInvitation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val target: Int = call.argument("target") ?: 0
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
return
}
"ShareFile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val filepath: String = call.argument("filepath") ?: ""
result.success(Cwtch.shareFile(profile, conversation.toLong(), filepath))
return
}
"CreateProfile" -> {
val nick: String = call.argument("nick") ?: ""
val pass: String = call.argument("pass") ?: ""
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass: String = call.argument("pass") ?: ""
Cwtch.loadProfiles(pass)
}
"ChangePassword" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val pass: String = call.argument("OldPass") ?: ""
val passNew: String = call.argument("NewPass") ?: ""
val passNew2: String = call.argument("NewPassAgain") ?: ""
Cwtch.changePassword(profile, pass, passNew, passNew2)
}
"GetMessage" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val indexI: Int = call.argument("index") ?: 0
result.success(Cwtch.getMessage(profile, conversation.toLong(), indexI.toLong()))
return
}
"GetMessageByID" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val id: Int = call.argument("id") ?: 0
result.success(Cwtch.getMessageByID(profile, conversation.toLong(), id.toLong()))
return
}
"GetMessageByContentHash" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val contentHash: String = call.argument("contentHash") ?: ""
result.success(Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash))
return
}
"SetMessageAttribute" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val channel: Int = call.argument("Chanenl") ?: 0
val midx: Int = call.argument("Message") ?: 0
val key: String = call.argument("key") ?: ""
val value: String = call.argument("value") ?: ""
Cwtch.setMessageAttribute(profile, conversation.toLong(), channel.toLong(), midx.toLong(), key, value)
}
"AcceptConversation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
Cwtch.acceptConversation(profile, conversation.toLong())
}
"BlockContact" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
Cwtch.blockContact(profile, conversation.toLong())
}
"UnblockContact" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
Cwtch.unblockContact(profile, conversation.toLong())
}
"DownloadFile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val filepath: String = call.argument("filepath") ?: ""
val manifestpath: String = call.argument("manifestpath") ?: ""
val filekey: String = call.argument("filekey") ?: ""
// FIXME: Prevent spurious calls by Intent
if (profile != "") {
Cwtch.downloadFile(profile, conversation.toLong(), filepath, manifestpath, filekey)
}
}
)
"CheckDownloadStatus" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val fileKey: String = call.argument("fileKey") ?: ""
Cwtch.checkDownloadStatus(profile, fileKey)
}
"VerifyOrResumeDownload" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val fileKey: String = call.argument("fileKey") ?: ""
Cwtch.verifyOrResumeDownload(profile, conversation.toLong(), fileKey)
}
"SendProfileEvent" -> {
val onion: String= call.argument("onion") ?: ""
val jsonEvent: String = call.argument("jsonEvent") ?: ""
Cwtch.sendProfileEvent(onion, jsonEvent)
}
"SendAppEvent" -> {
val jsonEvent: String = call.argument("jsonEvent") ?: ""
Cwtch.sendAppEvent(jsonEvent)
}
"ResetTor" -> {
Cwtch.resetTor()
}
"ImportBundle" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val bundle: String = call.argument("bundle") ?: ""
Cwtch.importBundle(profile, bundle)
}
"CreateGroup" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val server: String = call.argument("server") ?: ""
val groupName: String = call.argument("groupName") ?: ""
Cwtch.createGroup(profile, server, groupName)
}
"DeleteProfile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val pass: String = call.argument("pass") ?: ""
Cwtch.deleteProfile(profile, pass)
}
"ArchiveConversation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
Cwtch.archiveConversation(profile, conversation.toLong())
}
"DeleteConversation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
Cwtch.deleteContact(profile, conversation.toLong())
}
"SetProfileAttribute" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val key: String = call.argument("Key") ?: ""
val v: String = call.argument("Val") ?: ""
Cwtch.setProfileAttribute(profile, key, v)
}
"SetConversationAttribute" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val key: String = call.argument("Key") ?: ""
val v: String = call.argument("Val") ?: ""
Cwtch.setConversationAttribute(profile, conversation.toLong(), key, v)
}
"LoadServers" -> {
val password: String = call.argument("Password") ?: ""
Cwtch.loadServers(password)
}
"CreateServer" -> {
val password: String = call.argument("Password") ?: ""
val desc: String = call.argument("Description") ?: ""
val autostart: Boolean = call.argument("Autostart") ?: false
Cwtch.createServer(password, desc, autostart)
}
"DeleteServer" -> {
val serverOnion: String = call.argument("ServerOnion") ?: ""
val password: String = call.argument("Password") ?: ""
Cwtch.deleteServer(serverOnion, password)
}
"LaunchServers" -> {
Cwtch.launchServers()
}
"LaunchServer" -> {
val serverOnion: String = call.argument("ServerOnion") ?: ""
Cwtch.launchServer(serverOnion)
}
"StopServer" -> {
val serverOnion: String = call.argument("ServerOnion") ?: ""
Cwtch.stopServer(serverOnion)
}
"StopServers" -> {
Cwtch.stopServers()
}
"DestroyServers" -> {
Cwtch.destroyServers()
}
"SetServerAttribute" -> {
val serverOnion: String = call.argument("ServerOnion") ?: ""
val key: String = call.argument("Key") ?: ""
val v: String = call.argument("Val") ?: ""
Cwtch.setServerAttribute(serverOnion, key, v)
}
"ExportProfile" -> {
val profileOnion: String = call.argument("ProfileOnion") ?: ""
val file: String = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(call.argument("file") ?: "").toString()
Log.i("FlwtchWorker", "constructing exported file " + file);
Cwtch.exportProfile(profileOnion,file)
}
"ImportProfile" -> {
val file: String = call.argument("file") ?: ""
val pass: String = call.argument("pass") ?: ""
Data.Builder().putString("result", Cwtch.importProfile(file, pass)).build()
}
"ReconnectCwtchForeground" -> {
Cwtch.reconnectCwtchForeground()
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
}
else -> {
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
}
}
)
return
}
}
result.success(null)
}
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
@ -242,9 +546,7 @@ class MainActivity: FlutterActivity() {
// We need to do this here because after a "pause" flutter is still running
// but we might have lost sync with the background process...
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(applicationContext).enqueue(workRequest)
Cwtch.reconnectCwtchForeground()
}
override fun onStop() {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

8
build.yaml Normal file
View File

@ -0,0 +1,8 @@
targets:
$default:
sources:
- lib/**
- pubspec.*
- $package$
# Allows the code generator to target files outside of the lib folder
- integration_test/**.dart

20
index2.js Normal file
View File

@ -0,0 +1,20 @@
var fs = require("fs");
var reporter = require('cucumber-html-reporter');
const reportRootDir = 'integration_test/gherkin/reports/'
const jsonReportPath = `${reportRootDir}json_report.json`;
const htmlReportPath = `${reportRootDir}cucumber_report.html`;
const reportFile = fs.readFileSync(`${reportRootDir}integration_response_data.json`);
//const jsonReport = JSON.parse(JSON.parse(reportFile).gherkin_reports)[0];
const jsonReport = JSON.parse(reportFile);
fs.writeFileSync(jsonReportPath, JSON.stringify(jsonReport));
var options = {
theme: 'bootstrap',
jsonFile: jsonReportPath,
output: htmlReportPath,
reportSuiteAsScenarios: true,
launchReport: false,
};
reporter.generate(options);

View File

@ -0,0 +1,60 @@
## Environments
Located in the `integration_test/env` folder and managed by the hooks in `integration_test/hooks/env.dart`. Specify the environment you want a feature to run in by tagging it.
* `[no tag] (env/default)`: default environment to load if none is specified
* `@env:aliceandbob1 (env/aliceandbob1)`: no-password Alice, Bob, and Carol profiles. Alice and Bob have already added each other, Carol has no contacts
* `@env:persist (env/persist)`: changes made to this profile persist between features and scenarios (but NOT between runs)
* `@env:clean`: runs the feature with no profile existing yet on disk
## Tests
[ ] 1. general
[X] splash screen + clean load
[X] setting save+load (TODO: dropdowns)
[~] tor status+reset
[~] shutdown cwtch
[ ] 2. global settings (verify functionality)
[_] language # blocked by dropdown
[_] theme+color theme # blocked by dropdown
[ ] column mode -> background? so all tests check both modes?
[X] block unknown
[X] streamer mode
[ ] 3. experiments (
[ ] group chat -> needs many
[ ] server hosting -> also many
[ ] file sharing -> a couple
[ ] image previews
[ ] clickable links (how much to test?)
[ ] 4. profile mgmt
[X] create+delete
[X] default+password load
[X] name change
[ ] password change
[ ] known server mgmt
[ ] 5. p2p chat
[ ] add, remove, block, archive
[ ] invite accept+reject
[X] send+receive
[ ] acks
[ ] try to send a long message
[ ] malformed messages, replies
[ ] overlays (invite, file/image)
[ ] send
[ ] receive
[ ] functionality
[ ] 6. p2p settings
[ ] name saving + transmission
[ ] block (ui indicators, functionality) inc in groups
[ ] history save+load
[ ] 7. groupchat
[ ] add, leave, archive
[ ] send+receive inc acks
[ ] try to send a long message
[ ] malformed messages, replies
[ ] overlays (invite, file/image) inc from non-contacts
[ ] send
[ ] receive
[ ] functionality
[ ] 8. group settings
[ ] display name

View File

@ -0,0 +1 @@
_âeK%?Š!ţ~Lö9<C3B6>u×ÍlýQQż¦U•rMQCN5<4E>T-Ó/[<ń<ěn@KgŚă-ŕóŕČŃÓWÇ^l$řIC]»ÎI× ů@z¤m•Şb ŠNgířż?ő:†IşäD!ă±6ć°%čě…b

View File

@ -0,0 +1 @@
¢‰qö3‰ ÉÌ¥êÒŽB7 Å¢(ÊvQBöÞɱ<C389>øŒœ¾F±zŠ\\UƒÈG[Ü/ £Ñ?uš¼\;]y”HþG|þÛ,Þ3xÛÞeE0 !¬ÄSÍž<nÐÃòÐÉ®M~ üw “ÀëQ@6ǸËÖo£ÉüØ…ÕöÀiò

View File

@ -0,0 +1 @@
;KĄĂ”ČÓŠť\|ç<ÂŐÉ^1iRüÁw°ôFŔQĄ'¢©©z{P4ĂP(ä"5͸Qpr7˝`ŇK^uý¸ČÖ;©1&Ĺ,vŞ K/YößžŹmĄâ}±3]/§v"&ĽiѸ!3Wîyëjuvą¶D+w_'

View File

@ -0,0 +1,2 @@
¢ž»5‡Ä ô<03>m-J0újÕx­ ŽÙð•ÛjÙß“K×çøs³C=íà¾t¶-ÿD÷ñÇecàIXF`íI´
÷³6 Vr×gp4ËBóäÞS¿E<C2BF>tvìíä1iù¢‡”}ûZóÈWMóŒö´»1þ‡KB||Å,¢fEž%<<3C>D

View File

@ -0,0 +1,411 @@
dir-key-certificate-version 3
fingerprint 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4
dir-key-published 2021-09-01 00:00:00
dir-key-expires 2022-03-01 00:00:00
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA7cZXvDRxfjDYtr9/9UsQ852+6cmHMr8VVh8GkLwbq3RzqjkULwQ2
R9mFvG4FnqMcMKXi62rYYA3fZL1afhT804cpvyp/D3dPM8QxW88fafFAgIFP4LiD
0JYjnF8cva5qZ0nzlWnMXLb32IXSvsGSE2FRyAV0YN9a6k967LSgCfUnZ+IKMezW
1vhL9YK4QIfsDowgtVsavg63GzGmA7JvZmn77+/J5wKz11vGr7Wttf8XABbH2taX
O9j/KGBOX2OKhoF3mXfZSmUO2dV9NMwtkJ7zD///Ny6sfApWV6kVP4O9TdG3bAsl
+fHCoCKgF/jAAWzh6VckQTOPzQZaH5aMWfXrDlzFWg17MjonI+bBTD2Ex2pHczzJ
bN7coDMRH2SuOXv8wFf27KdUxZ/GcrXSRGzlRLygxqlripUanjVGN2JvrVQVr0kz
pjNjiZl2z8ZyZ5d4zQuBi074JPGgx62xAstP37v1mPw14sIWfLgY16ewYuS5bCxV
lyS28jsPht9VAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA+jzmadukj4Q0qLgJ0at+nDXGruO5JD3HsehobiwO8HrdaaImY+rY
CZzxRWM4xryQ2AFuAGbSxGoNQT3dTLvjKNVdGY6jPzlS7vxKbPeNZtc/YMvfZ+Fx
uEjvaZ6nDbviVtQhtE0J2EZ32n90Ob8YC8l/7zh0hp+mZO6Wf2DGXWjNvG7d8Ucc
p5A1ZVIpJ/VQzdlPaocO+6AvxvSBpaIUF0yGpTwofTOjtUmZyuWbhRndsQj1qMcj
e8wzOIgr3HZyhO9wztQGkZ8bzHq65oZe0IIOXZu0icZamFGQ5I6y5duCqxDDe4C/
v1/6bD1I+/ujLXRMmkcbJ3NZE+KrZg7KIE5ScGbkJIX7vIicqtsf+7VipdOh3/wp
qaDxX9Sp2cbVUU0M/aJ14nDSeFlx0XQAgWkPjG2lYtTNEC2zuudBCuCD8es8EhAW
FrU94cYg9lVId0NDMOpWPMH2QJFS4tk3Hc66si3+gkCOt2GOaSQeD+gGWkdwDzn3
S8iAur2GohFFAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
B4914rKqUc51Q1nq8CrA/e7EaMQ0ug08qlBqWyzZSDiBKVCoQj446ZJMU9VKlzJF
XtuURlJ7zswXMze7HceakrkxZAc7GiAGKO5hgbbI8XGLvXn16Lsr/MP1cmbKoI04
g5tG9Kx6yOB4r/l2TQY9Tw22YcdJ24W2/mw6TmDv0b+IorsIBnxIDv7Q7j25IkNE
hW3F9R+Ntja1RWPqKnptp8nxBt5/2jVr637BFczDv9K509QX+HHKyICA1hnvDDU7
N5Y1/mVu4JwQrBAFL857XbobP4QaLsZ34Q8LRE4dveuyw+vjVa1YimZ6h/RvrYyP
8DUi4XnzFyztecivXbdSTpMTSMfC4NQXFeT+XStRdWlapZyCFhp74w3wv7HCB0z6
7QT1HWMKPRvj1DsHhvPviyLVCL2tl2x+G7aaledOPf6BbhO7VolNeHiubyYCQl2H
t/Vy72DZbQeuLhf5GyqVyUm9uugzvVrryUiNUApOW8Xta2dAEBqinDrrY6iMYxh/
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
UrJN3Ey1hSHTaMUAhINCVFhojt48ppxky0bvwztQ9p/Vy7dfRx0APNbL70/XZOrR
sRj8zxtx2+tc5Lnkfaah63bmVsUNTgD6LudDaffXiV8XhIeVbzS0r/YJ0U1OsbK+
ApDItNDUz+VIJL5JUDjq/6fojFlWPYNIwyk5G8zOM70Atjk6UDyCIihV2u5pofW3
znFaFp/XhC14S8lMPZYKbnyl2iQ7UsqLpTxg3EwivIlSVFs5YQe0yXgJFX0oNd9Z
gAf3JIonA2g8Oo9EkgRfYCI33AwyVoU3QN1/AmLH2uPWTKhMu7k+OHktuIBfyFTR
9jbUq+YTU1ni6kEsJVBP/0I4n9Xb4VYIoqOq0BrcEp3lQ8BCEWjIGwLh1HYc9/DY
meE+cwLp0RNU8cuxyrGnkLA350bsNxrDkiaHAkj5ZA8W9VTGYsBxVhbLdQzN3GOm
63GJBgjdaOsD6WXs/737nD2sLu6dnA/Jbz84ouZSafQO/FNQZnndfj4osjabmq8O
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 0232AF901C31A04EE9848595AF9BB7620D4C5B2E
dir-key-published 2021-08-01 20:00:02
dir-key-expires 2022-08-01 20:00:02
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu9O0Pueesn0+29BlxZs60mBqehjdQtgSnKOm9QZxbQ0xrMQgbFnR
hWbKD8erenyeFk2SF6AJkbyzgYC89hyPW+8GBDmg5bE8fRKjgV/nI3tY2m4rkY3u
zSmYIdwqHUUc98Xzt9PaQ8IJAlDBY4XLKrWmJMxSyhBlVEept7+9Tj23qowW44Mz
xPJZ1aFkB1FpkD6qmoCzVZbhXy3cGt1nDwdJK7KqlaXziz9pFiw8PzTVU2xFgJNy
+nEcT72DBtk3G5K2Riu/aXY/D541Cioj9KMV4Nv4g8aBKx58Xq2tq1pFkc1Bqj1y
2MomVR3iskFzlqC8yKWGVe4OP2IaOhtcQJYp5GR9q+dWnr53WWNVxNu3sA9iMal3
PJUk5pIYrsmArGew5gmlCe+Al46nPINxc7ouztmStAV+2F6SpZlKOcstnT+KJ52O
1xnOSaj/WnzG2o4KZ9UrFQoUNOLQJcelPcC+vrinMk9BQPcB072l9NjpUBC9brsW
qTCMStn1jfDDAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAp7nHn/R+ZZ8lza379M7BJ00JYPAcncjtoa2K2Z75bDoxlegGvZXp
j4D0WhqksaaOr/+YCSPMcs4HAapKE/Dj09p1kjzh6Xu/iVp51NiQAARS5j3tu/5k
WJQ7ig207TdtjmslZIx0UU8pieuenRdyUN0PvjOkaoZIpao1+UlIe47DP+42D3QX
1J2wu48QDvt7hUUA3y7yLUyNMarqYBbbXQ/MpH8tcMT76TTN1uilP6W/3j1b6Fr7
NGtbUrS1EzOOHnCpgpnD8qGcisDKrHcVkNkh1w+8LW9ef7RGpFPpn022hUQG0WLD
5zrh19SAsKetWAZY6RlvyCHPVReajIAovwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
ca3I8mHu2zEOCnzySzdk+rbZLpohw5aa3NmTGFzRUXqOeHClOYHRc+glAyCrtUA3
lEa5fiFaZTImKu0J/uroyR4uF5JpzLOfojTQi9P5hMCBSdd7uGzoKC+/dKb2OngZ
VkBjptMf1S9dy2lUdDksHnnyg8UrV7EolIHUFNdEBI1LeONkdesZ5oQMg3HRlVpU
v+m/7y/MB+o3KAXkQyAxTcV4bKdsHm3Pf0CSfDgOPImwFS4lwyEW0STlOmVHojZR
5wm+5dwt9vbD7K6ectbnWtWjiSrvtGjqixO652lxz1qrsid99S5wEzJNhfif8lYe
VsB9h7YagNHJHLiGeBT1kg==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
ZkHpe8JVvGsiAsH8gw1eZVIIE8WuM+3Sdd37U2tOyDi7FVwJV+oJ+aKwcCTqTLaj
jglQJbg2JdV4ofy49ZaQa6FBGLrzxAS6Gx0jg+28Kzbr0xu9hSX81oPSXKn9KDLr
BvmuSqKBB+5B9nIEBjm6FwPc8MjqlvNesuJ3IpW9+e85eB7qsH4ozjHF0GIgpXu/
qXrk2TEK1nMc9EN+VCYuy3gAm46GHQEYR1U7gIofCYf7LQpDrfj1sAGquCQ3vYqT
Ex3GtqcDV22IME67Cou5rv9OmMnmy1dbeHO4g843RX0LXtEDdGYGSLHzl8EAscrg
i55XFlS6z5OwCbdDvFTkHUWRlaiDtoymaxAEW6GUmNjHhgWY9wJwgroVNRsP8Ihi
aex9HIND1MY4ERS41Csba/0grf+FahMVI12gwpmrnKfF95QHWw2MEvT1pzZGtMnq
XD8mcVNYJtcTvYM/cUa0I4BFD1AyeIP54hEXwIsqHm8KBJpjX/ZpPzksnc4NY8i0
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 49015F787433103580E3B66A1707A00E60F2D15B
dir-key-published 2021-11-28 16:32:54
dir-key-expires 2022-02-28 16:32:54
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAxVbS0noZKz1Ei6858RGyyuQgwQUKG4Urrp2BiAzkYxwX+6fURlut
AjeLb4XysqCdNdUipuLRQ2QIy1C220QiCHV6jZAsM4tmEq6TpK6q1lxi5YPKqbGS
CfUQFT1nO4s4DCYSLCwiRNy6bMe8tNHc0MpXP3loCbPkYCoXrEL6vYIROw3oeGWE
KbFPQrzYJAPHgUubBibsY5lkUY9N/5QZw2y1bn+dq9mFOoCIHLd6DkQmySmftnMe
QrpYA2WvE4M5yN2HB8QGT7TdzXPPL6889rFw/mjqYExQPX7cqmILkchsB7I5whjA
u0oodF8Y9ooK9QT0GeK4h3xQhzNG17anuUxbZ7sxzmBwBNmkNyLWEeIntazyjRFr
P2mDY/9YK2JOQKkh3tKl1whcCG9ZtAhKmm/ijG7OrhqtusdGKBXIgALf4f111AK1
gNcacDx2fJzRHuNK8zkIORAzStxKdLbAbBNeLENk1zBjSkrxCOJH4mBpr8TXULq1
ThLI/8OzZq4LAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAo32l4qg46cqP/sAL+oLmQM0mDiQUy6EtNa73vyy0BJEGWJeImUO4
gHNg9pyMFqyF+rP824gAzwX9Un9HaKgFpIrsKcZzg+Yl2vlrBQpJ0NPIkN9oqj27
W/A7RftMhH2itv0v87QudD7FqJpxdYNf3wpr9GvsAiHZMBfC88WhCnmJoDBwyucY
HFH7gzjPeDx37KD57o2M1KC/SRVtQtrccA/WzcxNypgAYkJu4yE2gaDr2WFn3hFv
kscW0jn6+157UuKH0rCNeRFDx8SsSS0nr6Zk/n+dlXzHGDO3vQIKCoRoH9yL4T//
hkMYE/4qc9R49VyXxK+n/qU6HQYpQMi+VwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
KKrOMRAg1bx+XFLRjhQB5OFjtupkqkFnGjS3LZQ5FHNwJ7cKG1X7K0aJNAumRPMD
w9xraIMuuok46wj35P9Tcy68qi3EqIJP5378ZtdK3Ncy9KkSWJSA9MLPmB3fClL/
/5TwboePXRdlt++Bcw9OC83HDuFVBqZArFIvopKf/AJOyViRVHlBmgNKFpm9RJTo
XsD415rJAi79tAfXzKuagke6DTVqobMhxrUmp3RjbEEEC6icQ3YX9X6NOPQ4Gwl2
bpWOVi3/9EGRge0X8IYsqB6/pnEXM2FSOTMdwo4YQzIgW/HLE9hXjFCx7QcPkcos
AZHvl12tKzZF3F9MKPcNyQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
wb40bfNLWyU9pWW+2BAHbrmZZfbs9UEIS/6G66VE6823/r6M90RCmFx1JlwqgmaV
/WMbBE9DxFKILyhuQi6whIKoYndg72VDeZL5BzWctalw24VpJePVjeGLcTdJHBVh
a6UiQFaQdH+bTX6qNIFv9nNwq7ZzJRBvaYF9bK8kaTrZilFKoRVBxXssBUFjEz6t
f+sei5WIiBnzaQOUxdMjvdDAHci4DXwGw1U2M7jcYARo4FfvWkAxzWLxocWmauPM
8tDn0fSgMnLlSOR2crnriQMFhYD+9xyxfOq1IDH2IWCKlejz7j3DHSqBYiUSO9oD
uX6htwbMWwZQeqt+LttE/zZX1Tcv6PJqemT8uabH0s94W2A3sJpstWJ+0capb+Mj
bvTXj7t2ilqa5RX35KKhaQ6wlh4OXZb2ydeJZc7wtyG8eN53aVqJNJQ+WZn4IiTq
fefr2ojy2VDJLDHJVNpKQQzmjXtSs+69wCvrqdHGjGAQl5L31LjZgaNLNj14RI+H
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint ED03BB616EB2F60BEC80151114BB25CEF515B226
dir-key-published 2021-03-29 03:27:58
dir-key-expires 2022-03-29 03:27:58
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA1d6uTRiqdMp4BHBYIHKR6NB599Z1Bqw4TbOVkM2N1aSA4V/L/hKI
nl6m/2LL/UAS+E3NCFX0dhw2+D7r7BTJyfGwz0H2MR6Py5/rCMAnPl20wCjXk2qY
ACQa0rJvIqXobwGnDlvxn4ezsj0IEY/FEb61zHnnPHf6d3uyFR1QT06qEOQyYzML
76f/Lud8MUt+8KzsdnadAPL8okNvcS/nqa2bWbbGhC8S8rtDpPg5BhX2ikXa88RM
QdrrackdppB2ttHlq9+iH3c8Wyp7bvdH8uhv410W7RnIE4P+KIxt3L0gqkxCjjyh
mn9ONcdgNOKe31q2cdW5LOPSIK+I5/VTjYjICza7Euyg03drpoBMGLuuJZY6FXEV
auIBncWe+So8FMxqU/fwo5xm6x085U1MwXUmi4XDYpr/kau6ytPnzzw9J++4W9iC
em5Jp0vaxrDnPdphqT0FWsBAwsZFL7nZRnmUlTgGsXUa0oSM9/MErDwzELh/NwG4
DNyyzRG8iP61AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAsw2ZJlGsmfDmDwoKbzjOno759Xwqn6JX+tFasI8eRjOFnOyjYzd1
XjG6Gj2hVpF/ze2NiTuUyRu3Ybp8G9/gs8VGPljxSHkEugGjQdYFoST02ma0vUHA
8YqpBYOiLvsXnqfEkl3Yj6HVxmVJA9j8BxODODlBtxRMJWFrpp/b+qCo/YyGmCh3
n0qd3QNqFPLIzwvjWVhaFfga8dXBT73wX9uYT7nT/e3pV7ZvTw0caqi7svNzj0I8
/OxOEjoBQEQMQVPT2bNZKBe9X8QKDSgdealZQwBT9wdZ4KndtCj6Y8MVjj15/YtH
fWfNyUHgVqOmfDK7m3pHXR9fGgsLQexIfQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
GyN9MMaPABXJ16WAFIhdzOhNT59BI0RAfV9ZpUJgzkAAmpoj+WwUtWfcrW7a08CT
9g60QwJonP/Nh+8iLvQYH5ZyEqsEj0HEUD/yI2kvN41Y5QBD1Sku8Cu4E2UaICzL
V63oitjQzppKlVXHyP/SXsI2bUjoHLtT2pBvxRJ84DlZBEQ/ZqS38NN/+Z6DtMR/
kn0l7W5yA3bYWzeKy1TeKLWo7p0hHzv/Hswe/eha+27LuwZZwwfSQrRy1fi66Fmj
0xBP+iXXtmNleFegFuhEBPXa+9udrT9rodSdazkGPzjyF6HWRMP5DtmTI6ovJDVX
60UQ0hNb6KAP+FZKPz9/dA==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
zAgmCR7tf0btsogvBmdxJ7+RWjPBzmDSA7f+zlK0jOc3lDDD4pxuQO6iNcoEDYMC
5hkzVoGBMYsxLfLZXFGE518dn79iKr6SQoq87AhnAsIiAfXMk2AWLkWI7MTzZo2U
dcgo+7vzxMObk86vzFxYWrSqp6CSZ7RwBRnH3vCGLfOMZ1lXMug+MQAQYAyl6KIR
3d/uEu3+sPFJcHQFP3C/7bHDG5j/76kwoFzjSjg974rSjr1j1FbrpNn35mLc+2X/
11n7cOADHWaSN3MlLWGsYxuuX2l1w/XZNfFEezDwK3BOotbj5spU2nQ8xbDFPB5+
ixDfc00TC3YbveSz+S8W9czfysJt3KaWmQczDtSIXag1qrL52CBGUVGP6+R7xnpR
/4QD6yCKmDcNk2D1YnindwYC48ydDt/u9A/97cEBpUbul3feW7eKLk79MIklWlWo
3c3aQVH6Ewrb76oXYYwzNbqJOp2ceREu72/Fk/keprVcupVDtVoqHgDDpfOUYTJd
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint D586D18309DED4CD6D57C18FDB97EFA96D330566
dir-key-published 2021-09-06 18:42:41
dir-key-expires 2022-09-06 18:42:41
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAvi5+A+XPw4jxMYhmEI4+MpnaX3dUEbsMGHA+xAMnmVhuxbm3Dn5c
TyhQNY2LOlsieE84UYG+J4dABfaFH4w0l6zUJkuytX4+6WRQontw9puR/IcXkRwM
8Tv/tY675OYRCm9DgDAWfqZM0IgTzSrYRDl8eFPSFCOP0NhMrQZeUrdKgwAXVZWP
xt9nTCwT4K9BMp47LEmZKdEokeVsr0l29Z9v5+r24k9x8EQjDexsoHwlVrxWfarG
1klWssfSFpkMN+FkTQnBC6ByiBh5ZKM5AC/HkVFvuHjehUpfrtNk6XNFcKbDvEIg
qPdg1QWuuSWpZVA+/EwSBtwMNcq9pv60L8Cm9WCJoSC691WByiGwFCy1/XcBI4J/
BkoMEvP3kAxzm92jqGbpFSJawFRPZKy89FDKpha/So3CERQPV0ar+DTpVqDlryWV
N4x1IzpPeSHFj7T74q8qdrxx0wcAjWJ9WYoGQif6FK3hHcmbSGSgyvAFeoYxyUCL
JHkjBCD4WTWVAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA1Hguh3PNTfMd7kLD8NupSdye1KB6hhQitx8DipvT71ZaCZYI8fZ3
z5xa6fCcJXv/xoz1tzKeJ1n4/AzAbc7ltCyyWkj9CbiB99cEe+sVE9R899eFvPP9
DUmsmgy0Bn2MrdfD/N5VeJ219TTtqI75BJMd9n1+r5zUzhji2ihcLWYgi0GVZoec
6B+xfPtYbifCdrPRBwrMAW4EhtMKeJfzsYFO220f7x2OmmZB9muesi5O8/0zjwu1
xOKldXCFbccTfFN88nYmaO8j0SpG9nOveFXavPs0LyVzhuMkbLXSWAN+M/S6GC4L
1kbkjQ6YhuYSnKxGFo/wdax41jrSFCf3qQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2a4uvuN2LUgSuHoWYckJusjzeaEfTTN+DHJdQjJrMueZtxhhz+C+soYoSAvBsHI
huc2y0wLbeNMpLXeyGoYh0M4zm6RyjMksHQZbgPjkibflbUI3csJcvaBMQonfo9B
knoqOzeQd8NioOlnbYQ+k78swGtg2ndHpK4G4NMBK6ZQrbhrIk0nVhOhaIHpGdiN
icr+czGq6SzH4Snp26dJ+J+9SAdTOzgat/C2Othdu122JR2/7GzCnz8dqS3LabN9
iWJWMLxayFKi+Z5f1WjCNOVh5lSjpeLjUNSrA1hXXKSRD0eFOQFRvgvO60gyvooH
C8amqpSD8HqsCK6MvN7V9g==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
uuQhm7YYbqR4/fGSDWqzwiw40nr7y3laCmfiTDHOA5N6lVOw/tM4bdMbGH58wkkK
XBIEz4zQnIT4Sgaumc9PZK3/a8wkx3HgvSNZAEvv5GK2iD8QQNaR0mv7/gbCOLeD
4MAsWH7ehc2u0AcebYehYWE7/fknYRfIGLRzeAeR479LgtFIaaZ54lGeEWKA7qBc
B3njJcNDlekChydxw2JRMw2GmK7Gn/cVRLjFiG32aaTPA37Ietw6Z8wXEjTy4087
KTzTe6puX0g9kCWMaGIBzod+ucNOG9WhgVfy6M+OMddI4KbgizUM7a5c3DZwnQHk
nn5yqib/W7NmHZOL1k2qYlKQlbr412bsDgBDoFYSYPIkbO4x7LHJnGGiwxYx4vmx
caxDySQtqCcR9ygMrZVrL9W/Z+w2N/KCXnL+SgTmN0x/Saor1ZTkONj5Tfn4dg/W
xDxvLO02DpVTfgidUsBeHSnMQn7w0iG0abhWFmYNFDjxZFEWy30mRCEYADC/1NCI
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 27102BC123E7AF1D4741AE047E160C91ADC76B21
dir-key-published 2021-08-21 23:12:45
dir-key-expires 2022-08-21 23:12:45
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAuxgnMVH4vwBjMeGvrEODOYcjbCS4N+Wt0SZ6XA5I08HyMf5AbaaF
MDscJBRIUOp7DyLmUwK+jp+QI8pUjjKsB8S0ctb/J3Im2T6CXnP2KgEfVmpNVQmV
XdMm8cRZl1uIZDDBAXizSQ51f9A17TJh7pF/5khYp/SAzl6aO5ETn7ry0ITiJnNa
6cY+400F7ZBA8NuXnCHVGfmpFFsiJKFrS1Kve629eeaNEd3mynRviBXJy5a4NEGf
y42Ev8on6SxEnF9OG0NMJ081/+mP+j8Dsl3+Uehzr9B42MQQfDo4RdYGrt9XolBm
L4eay1ieZEsFeDy0TMfiGGbr90wo1fgGLHIRSfTNLhhPJ/f9cTZPe98rhSgGWiAd
RvK5SljoIOR4qdS9/aiZkj1P+etvh1rIQUcG4/xCOBnouEBK+DDHZFqyMtpMPtV0
Bxi20DVaMJcyhdfjVqcRSyuR8tlOnTid6QwBj6kgIIfMaC+4Ht6yO/SYquCWlaZl
y7Pu7li8WyW9AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAu1QJ+qlCbtrYsg9QENtOpvMrEDc+WgH1ZFxEqk0v/ad2ACQYe4dT
g1gJ6VZyGths3p8+WQXIA9YbcEr2oajXlLmLT2QAlqlsXMPKwwIpeG4rvR43Wwq5
mQ7aX+/VjZ9YZNoZVEAy1O7ti7GIXJzJYDOdgBjLifSjpjcEmSaf+v4E357azs9R
ndLHRRwbBLdUl7G3aMkL4ejrv6AAXexPxAL90xsb/MAhVEOQrJNcVMTgII0fSf56
P1J17SQwthNZ4rTMo2O9TvWUGNf1sMb9kdm+A2Nwo2CKmUR0uo5wHN4YHSBYFDcb
hxRtZlhSFfBJvJgrX8/+CnJrBFC6S/7vJwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
VzdxGiA6egLcZjm3hHrTW+N+7sPwaEcdYR5/GW757MO79O8QwjOLJSa5dOF5yDWa
3Ie+QDB2Q++a3+w776zqpFsaGCuEHBCfU9pxatKdoTsExZdQnWHJg4u9YD1JcYvL
dZq9uCCWaKa91OjA6/U9zp/LY3tOPUWCyO4MHehHYggzapbcF5uaMG0AT0lZzaXJ
vh180N5YGMfm0eYXJqkJyX3WCZhGroh7o3DyaqtBSJ1cY4NzTNgPoCAi3J/XEoCI
3JzxfH97uyqQngR1yGb5KggdM2ejci3Ld9q99hjXlzYRtsobUQBTlZca2vUk5ALh
vFWU7GzcNIdDjKBUd+IhTQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
WldVJc0L3fByDY9D+Yha2/8Bw5nqRpHu8RL8lU8zXb0rhfiA6DN2aefSCG9WEay1
sTVs4zhS8N0+278oP51Lg7S+V6SmXxKZbF3I2zNaFMIPVspRA3OF0R9vCP1YMxeo
D4EDQXiRxNNeM9hAjmmLiE0j7ZMetZ88ewwsOOXAvPHmCth09nWdCYKwibwUW/U3
w1HXCHK4BdZ7XXkMJNry7kZP/H1/5oYyuKshue4+f8tgRa0xg0wbHDrdbrR1qBBu
Xg9Q11hV29RIdD0ZgugUdyGKlpRa6Mb3GD5DzN845sFhCjsPrYyZv7Xo/S+uTtN4
t59BtF1TjnsAhUk7shk2W3zzaauzUarkDA1v+mb9NZ3CFsNgfxt6d1yDDyuxJgaC
VkIfO9nX9dCORSC9Ow+XOq+D/o4FfSe4q0VvrJJILAcw2Nu1Zg4ZhHBeugP9f4I9
p/+ZMrEr+YAKkjPw+uIEdR0l/YZEhcrjymX4FY829LNll2AceqMCKoapFWuEzzDP
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58
dir-key-published 2021-11-09 19:16:37
dir-key-expires 2023-05-09 19:16:37
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu/DOrbv/4IAYvyxsy/6ivC3q5yCQBWLKHZGYKQa5G/3rem8wen0f
qF7y4ye6U6faWc5kcNMHEKMIeBzMErxwF345qoGHITxbbOWnizgwPgrdCwlK3p0H
1XZGU/TTjoaM25P+ZNCBvGmDQRAtgs2odnv+i8hpu6vrcAUZYXmmw/Ag1Ou2AlLC
mPpbjV1O5SMylgC4IuCBPr3iA+M1kKkvj4LmwU6pJxjAae76GLzzQ/Ffvi7rRpvU
2BHetjehk+7/t8izgbhT4VABtzKgrv9ATnhfEgPeT/WBq0E75iciBBAXRPF5kEA4
k++NPy21XpL7jkQ4wnMs2HyiFhHbUwbLcoyQ/JVq/WBboSwStYbkdizRpkhJ1eNg
LiD8CPWcZnhWZi9VWrwT0xl+Mu4v6kwo9kVnXhOfcK8Wni9FqiBu2tmNDoGPG1Ac
wptYQSIoujuLgn4MARREwo9cWrKp2w+D7Dt4U7U5OrXL7TXjonEKuEHwRhzz1JA8
7LXm/wENwn1/AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAuxJxHCGOw9DgNtw4wqi78OE3djdiLwbie+2CevKMRaO14IhuQGVK
w1PYsnTuVLVcJl3Y4QKQ4nnbe1QCiGrLq9wueQy7ZvBeZry3f+QD1Q/PAG19n6/7
hlhXclSOJ/jRah0Gi+QXAycKE5RES/Qn4F5fNE7MxzM0ZQHIlszZLNUrcpeLE9nX
avlqlSqK8FmLPOpOSRrdPtzKP2sjW9UUFVGMfurDYIC51hkZI/nyy8A1C844sfuF
LV6oYpYw5+soA122zBqGqP6vApwFCvWSDcGlx8xj1Irdo+JIDfK8vklu9P11rTWB
R7dZw9pD21reD0pf0Bipzneho6iiL++w+QIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
aMzjdOHri8Kmdoy0qt1a33Y9/e5vKkJQkzCKdHN34Il5FMMCkOrJ1yeQgZyp6mU4
jPSpUZlr1Iq52x5ers4fH4SybvX16BDq+p6+Zel9f5TpFg1vzdpJH1WOJ3ZoES1N
S8CpiXVz8flc5Ez6Dc7uZGSE2fYRl1Pswn3GuLfR1Wjw0VNp1VgHZk6xYXRk/YLx
OyjZTWEWAF/0qw3usXtvTvh6wGniVxr0rg3zZbesLXti4TAn3B3N6VG1TPOizna6
s26edpQ6RQPigAuccEwU5iaIQEGkIxcoe61qnPvAoWP3Jk/sZAGCqhbya0CBCH8U
pEW/OauwlDlr3yXEKh05aQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
XhAoF04YrM2zJUvrQuEsGhU53Pbf1B0jv5F4YkMlRX2y15rKXKI93vQTM1LbnYc0
ETkhSOQB2rpnX0bcE+K+x0sWXiMRtR1HSX/oIPDI9MNqHv75eZlEkSaDJHIsQJlj
Dd++tMHkRc49nNNo2J25J3TiBU0ecpVYYvtJzynE3W8tX3io6EmvTehkj2o79z0A
ax2A5JG65plch0ES2yK2jqgBEmkA/eZENDNQAaERXMFJbbpHIMBaGguwCEieJe77
JBAOxJFRGpL6MhMpcvi5MgEMqfAv3AhlBo93n4apT2CYR8PdCHUZyq7FrgwTSJS7
ndl3YmvxJ7wnyTXitw0GcSVeQaYMQV+LR9Z1VkmjIwRuHliUn7hR79pYqs3t11aQ
muW8jOrx+5QsiTLEPV6Hs0pzXc9XDw7mnJ6M2gxxF8fZCztal3TNLs9+1O22fxME
0VU1oS7SG6T4M1YOXgKFUP20gLl8sZf+3lGp3aLZIK8psR3vzggpaRSUKgip4Lqv
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 23D15D965BC35114467363C165C4F724B64B4F66
dir-key-published 2021-09-18 16:07:20
dir-key-expires 2022-09-18 16:07:20
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAlv6XS+VppPaQzOgor0YFlcXLWeXiMn5N3VBneXuw8maLOu9oPJ9z
2/oMQN8a+VOWTf+/jebGzOBK6MamXpgsIZPQWiT18gZMsYdR8mcqBYqVP3khwUWh
9QYkV+m+Auxa0TLzTrsi6dLDJ384XdpDweU+YJghMJNZ1NqiT8ogj84hxs5Tf+Qf
bn7EBIcU7SAKr5Lw25KrMb5e3AZSC5MilBS/KLgVTq/GiWb7pKd5pxGwlGolNX8a
PccZ2ZT2DrSQsct4wVxhSbUqANI3PfMpXvmUDxWWBgbQwLF02/4gi+13snlHtqwl
y1WjE55HVfx1CTX13SStwmF/N3SFtFf1qil3j5qrHdHtKlAYOaTfqab1eLVH1l83
LI5QWD7ri9GpPqIjlh6PuaHjaO2FW20SouZtS9jJKwi1l1G3ef1tSlha1cxkRxIp
U/ngvQBsoa9X26VfQA4MieZgVVdMVwjCNh2YC9aEXc/KxfcBueZkM1194qP88cVu
dOFYaftOkuGPAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA3OigVlkOvlx54wcY0RRuExNw2sPYHr8m8QP+SnzARDbrubvsKT0S
z/+aVWccgacBtihOpF9juQLHb+nqhea9s6QS8XAQ98bqm8foKToWuxnIRS9c+8e1
qcENTfh6U2Dr8ckwVcKAPtnLnPtbxuFF5UiqXAPA89ZmtqUPv+DfmDr5fdeb0bCu
Lo6TCFLQOcn2Qz1WsSv/2JRkSBy8pgaC01zErgv9oRVIzFfLn8YpfnWZkFiRGwX6
/GBLsS19SLLX0xLkPwQ/CwN6OkipOtYi6UNq0osHw9xfm5sCzcnltJShA1YtIp72
e1HkTx03a43uAKlJBo1rMD29stVJu9ABEwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2YFJIj1zR9iMPWRJYMDEKuLlV0Gbis9232Gog5sS06LpUFPYL6clLDf7eWAimPs
u8rUUP4JEjmAY7bWHyqbG3D5iljNin50W7kvY8ip+Vqf64vjNUXFDKUbi0iGkfVC
nfX67FL0JF74hqtCtMlS5QPvD4oLsC40DdmPD4kCulaSrMlmsFRGFdl60HeSLbeP
oopRA4yYB4ZGJxJUaSuMm6RrcK08G2l7vLfHpxhcJWQVb2fKB7Ds+AogZYnc6ZYF
hpGAP9y+Yn8TUUqPMhhZwLw/8eUAhtv8G2aBBxHyctlGvg1YFiquPP6VEn88h9GZ
X4d/mLOAQeYWEalQC812iw==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
OlLQuEDdagECnVy1Nke/C7dpS8+8XvgLK/hGgV/OpCWr+Gq8bL6/NpK9GP7EbsUI
NxsguJ2r1wkEGTixz88gdKWDCC3evGW1pqnsjkCk69gHGtCxmrFeiCxCSomaOjzw
HCnp3TcT1DA4EstoXUqPysVkBYkx8OBO6rGhvE+G1S4bVG/EJkPCMhjPlxX41ON6
NWvtf32dviA5W1BrYKWJy/v0pCsApgjZa6qpaLdgqQabG5YEJA0rONS1hL+AcJks
CnvkSS7iU/4jrDPfgOLoVxEkH68swLol2Qf0RFHg12EL5kf0xbPnfE84aQyt99an
6VAMVIy/tCxR2efZ0+uQmQk7S35uQH/PxZ3/mq6cDMw7+WZdYbrkyfmFK/A+yL2P
op71Ik0Xf0Qwd0qMhTZMVPZpZDQmxvr0j0r3xHia0Ez+PhovnnxqI9/cThRQ/ceN
jE2cA46H4ZfYn5OdCP+mP9L+MsqJYoHj/SigcIrXUX58R1D0JWVX2KPU+tVyQ03B
-----END SIGNATURE-----
dir-key-certificate-version 3
dir-address 154.35.175.225:80
fingerprint EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97
dir-key-published 2021-02-08 18:16:07
dir-key-expires 2022-02-08 18:16:07
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAwBmqdD+G0q3smN5OBFHCcK5pQH5G1GIpFJ1JxCVEp92tTK4ZHnot
9RzMfag6zQFqwLaJ+yEb1DOjTdTMfcUTsj5f3GUqPB+U7shSMAvvAAM+Bx/4m1AU
u6sk4XmPB1bCBfcRl4zhnY6XFIbj0ktuBDblcxHz3lDgHFpBoci9sF59mM14MZ09
EdwgeckcU5oeq6ApuSlUVaOT8xsKV/yeK4SKaFfDclwPAJuitQ5CpqctP7ExmlrY
sboTDtz7/Xa6OccaGDEUf7TRlipvUX6rvlmvHm3qjdixVfExpa8E5QG79GZTL82p
1zBd3iqc6QEnRDTiW9cMUeQt4EvrwOUVVYPWo3hp1C/iiNzWraDays2xuhaSB0gj
fPatu2CFW5XB2vd9IvIiWeklSFqnF8DL38jDL7DbFiETJreGsDMR03yHWVd0MbPz
OrvAxG4tJn+JtnwhzlbRjnfk53jOTbiM0vMV8h/ztapCiJeT/6i7nVQ1xL2boeYw
5RDUlwZaQiaXAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEApIIcKBWvD0P2YQtsrFKEF1kprJUCEUlWqzV4mVbTcVdzVQpct8t8
NAO8kDbxRSyU2S6gKecusy4H1MJWVAe2qvKIY974espuJwBXWFgT70jSBTFzjMpB
dAaTTY+kNZa66kjBjCVolr8UfFvL7HaL3CCtWD9ds7+ep76co1h3s3sD2BWW/M5m
V6ML8kYkjRW6SW8YHW6By3G+UuqRiGziJIIwQAoPnNSWrzW6UTLpVRDjdo70bQvU
vvfppUuNNji5SFfzSiakxHIse/eHG/rTNSzOvlpjuZxzPIcekr71eu1hCVHb2QdA
9Ikc5pUQeB0zImI8WJ9OVJDFUEgjJ9LGtQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
cy+VPbSGSJ5aI7egCwgNY6mgSlJumULFmUN8gfahvMo5hUwVLqP1FtoKIO8yBUc0
Y47pt6G5a0fjm6mjapFbU7IpqIUl+5gLBRKD6ugx+hr2IoqIVJY7WQUvVMBnfqHp
Z5N6kXfFBT+EbnbLiUqoRo1/AHC6E6CqI5pdhV86UCFydmuLf/MfwJpXiYRJueqk
DnPYEflq+Zu/RReL5aJlVOVuWq0ZpuzUHk4gIicKESLGkv4eI2CvuB5HTeNAB9L5
laMe+YpoXqgqMae1HT+rupPXYeONPygFXXbNLNVrR7OjAYG2TOaqdUTQkFefFVtD
ungKyPS6LTytSuU/rjWCbQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
sV4ouMb8pmCM3WjLTFxfRVb6qZl8rQ0tYM/AjKz1ufU0UmL7yx/7JMg2InDcYPH3
4PIUQrDLoAMxnnNVMDaDGoGDGI5keUmU1eSGvdJYN7okd0aRvA9LFGw4uDVVyB0K
l7BOk80y15P34R4G6oPvcR8aCxoiMh9UusbhiVjBr6dAfJGVVxZAO1ZJ8pa8fcrA
IbtNks2vut6Oy4oaC7zLCwcbRJM6dSvzcbzBpCf7/b9w6NQNqCBBQkqKgUl0FqKM
QRKqHWuhbqcL9+lj7rvgWCEigLu9ff1+E7C4BV7GzOm5FPcRqfkPaMsjQuM/HErH
swhf2Ra+Tcdk9gdI4AomqwaoD6B2uKsZkcFpZhq4HAle6rOP9eC16DpqsokpqoW9
vb5Mic7ABYVpB4t3o5wOI9D4exXmzv6gpuOyl5rJGL5ORYSEhnMGsKMyPceCGysg
SzwfPWBqRTM2LfBxhW05UEBJev4EXk7AA5sr6GkcX/CXeR47pyXQAyc2doZo7Aoq
-----END SIGNATURE-----

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,58 @@
# Tor state file last generated on 2022-02-04 13:53:23 local time
# Other times below are in UTC
# You *do not* need to edit this file.
CircuitBuildTimeBin 675 1
CircuitBuildTimeBin 825 1
CircuitBuildTimeBin 875 1
CircuitBuildTimeBin 925 1
CircuitBuildTimeBin 975 1
CircuitBuildTimeBin 1075 1
CircuitBuildTimeBin 1125 2
CircuitBuildTimeBin 1175 1
CircuitBuildTimeBin 1225 9
CircuitBuildTimeBin 1275 21
CircuitBuildTimeBin 1325 7
CircuitBuildTimeBin 1375 6
CircuitBuildTimeBin 1425 9
CircuitBuildTimeBin 1475 2
CircuitBuildTimeBin 1525 6
CircuitBuildTimeBin 1575 4
CircuitBuildTimeBin 1625 4
CircuitBuildTimeBin 1675 13
CircuitBuildTimeBin 1725 3
CircuitBuildTimeBin 1775 1
CircuitBuildTimeBin 1875 1
CircuitBuildTimeBin 1925 1
CircuitBuildTimeBin 1975 3
CircuitBuildTimeBin 2025 1
CircuitBuildTimeBin 2075 2
CircuitBuildTimeBin 2275 1
CircuitBuildTimeBin 2325 1
CircuitBuildTimeBin 2575 1
CircuitBuildTimeBin 2725 1
CircuitBuildTimeBin 2775 2
Dormant 0
Guard in=default rsa_id=BD4C647508162F59CB44E4DFC1C2B2B8A9387CCA nickname=regar42 sampled_on=2022-01-27T11:01:16 sampled_idx=0 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-01-25T23:44:41 confirmed_idx=2 pb_use_attempts=78.000000 pb_use_successes=78.000000 pb_circ_attempts=102.000000 pb_circ_successes=100.000000 pb_successful_circuits_closed=100.000000 pb_timeouts=1.000000
Guard in=default rsa_id=AADD84AF8D3C24F492D9EC1E08360D1C7CD5730D nickname=axeTorC sampled_on=2022-01-28T04:55:37 sampled_idx=1 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-02-04T01:41:15 confirmed_idx=0 pb_use_attempts=1.000000 pb_use_successes=1.000000 pb_circ_attempts=1.000000 pb_circ_successes=1.000000 pb_successful_circuits_closed=1.000000
Guard in=default rsa_id=F7B8A4B5F16ECDF6CA626F96F4E3C219D1A664EC nickname=kerneloopsRelay sampled_on=2022-01-26T16:41:28 sampled_idx=2 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-01-24T18:55:06 confirmed_idx=1
Guard in=default rsa_id=274A1DC6210E91827CDF40DC0E95E4A3CA929A08 nickname=AllanonTor sampled_on=2022-01-26T18:56:19 sampled_idx=3 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=891FB8477529A6B2894B7A3129402E0FD5DD281F nickname=hers sampled_on=2022-01-31T03:58:06 sampled_idx=4 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=898B2FBA56F3707B72A0B97CC492CBB466D9D707 nickname=julianrelay sampled_on=2022-01-30T07:22:54 sampled_idx=5 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=84A9473665250B752B621892834E71EECBD610FF nickname=flyingcubetech sampled_on=2022-01-24T19:47:40 sampled_idx=6 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=7DFC30D7EAAEF6E74B081EC0291757E9414A8C06 nickname=TorRelay1337 sampled_on=2022-01-27T01:43:00 sampled_idx=7 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=A5E42F1A3AFA948A7F2FDB1954A4CF6C6489D418 nickname=bauruine sampled_on=2022-01-31T11:16:43 sampled_idx=8 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=F9AEA07ACE06E8E7D55E10FFBAE037E8C833FA93 nickname=DTFNODE46 sampled_on=2022-01-24T22:18:09 sampled_idx=9 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=3910C5CA0CC5AFE22C709DF471A2B5B6B4AEDC98 nickname=criticalcat sampled_on=2022-01-25T22:53:24 sampled_idx=10 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=FD449127D30D8F5D124653D9EF736EDF4A12B4DC nickname=lw sampled_on=2022-02-01T00:36:22 sampled_idx=11 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=0E2EEC125A6AC8D5EBF5DB36B776A3DA5510E7A0 nickname=Nako sampled_on=2022-02-04T00:40:19 sampled_idx=12 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=2C7C9294CFA7AAFB3D25B72DEAED242E96927F03 nickname=ChomelesDEnetcup sampled_on=2022-01-30T05:51:02 sampled_idx=13 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=916DC3199F639168CD20AEC4D45969268E807699 nickname=defcon777 sampled_on=2022-01-27T11:01:04 sampled_idx=14 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=A6C3B64EC8EE20D77987EBC5E894CA6CCE4B5295 nickname=OwlRelay sampled_on=2022-01-30T19:20:11 sampled_idx=15 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=6FB33CCBEF5135AFA3D6D33CE030F96728EDD70C nickname=MDMIT1 sampled_on=2022-01-31T16:49:37 sampled_idx=16 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=85703987A509438D96E22AD367E99FF295E089AF nickname=gbtUSicebeer09b sampled_on=2022-02-03T22:13:49 sampled_idx=17 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=BA053C72E476C1EB9D05237D0D6A289C18FBE8E7 nickname=straDEicebeer02b sampled_on=2022-01-24T08:18:24 sampled_idx=18 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=225A8EA367DF3073433E0A845DDDA26D2357E4C6 nickname=Manureva sampled_on=2022-01-29T21:39:31 sampled_idx=19 sampled_by=0.4.5.9 listed=1
LastWritten 2022-02-04 21:53:23
TorVersion Tor 0.4.5.9 (git-d0ed04d50e80fe1c)
TotalBuildTimes 108

View File

@ -0,0 +1,3 @@
SocksPort 9667 OnionTrafficOnly
ControlPort 9668
HashedControlPassword 16:501107AD0642A3C66029D6A37845E976D5F55B4DED7C4A43CC044638A5

Binary file not shown.

1
integration_test/env/default/dev/SALT vendored Normal file
View File

@ -0,0 +1 @@
ó„Ý gÞd7èfª>ZrPòV`dB<(÷ ôÈW`½ 7¾c´¬n•ËnŠ.ü¾s8lÿ·“*dZUmÊŠí&¸ÊhøEëö8mê<1E>«Y ŸüñrÒý×W²H%{¸iùFÃÎ<äÿ[0²Ñâ”\ yÚø-¯R½L´¨ -'

View File

@ -0,0 +1,411 @@
dir-key-certificate-version 3
fingerprint E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58
dir-key-published 2021-11-09 19:16:37
dir-key-expires 2023-05-09 19:16:37
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu/DOrbv/4IAYvyxsy/6ivC3q5yCQBWLKHZGYKQa5G/3rem8wen0f
qF7y4ye6U6faWc5kcNMHEKMIeBzMErxwF345qoGHITxbbOWnizgwPgrdCwlK3p0H
1XZGU/TTjoaM25P+ZNCBvGmDQRAtgs2odnv+i8hpu6vrcAUZYXmmw/Ag1Ou2AlLC
mPpbjV1O5SMylgC4IuCBPr3iA+M1kKkvj4LmwU6pJxjAae76GLzzQ/Ffvi7rRpvU
2BHetjehk+7/t8izgbhT4VABtzKgrv9ATnhfEgPeT/WBq0E75iciBBAXRPF5kEA4
k++NPy21XpL7jkQ4wnMs2HyiFhHbUwbLcoyQ/JVq/WBboSwStYbkdizRpkhJ1eNg
LiD8CPWcZnhWZi9VWrwT0xl+Mu4v6kwo9kVnXhOfcK8Wni9FqiBu2tmNDoGPG1Ac
wptYQSIoujuLgn4MARREwo9cWrKp2w+D7Dt4U7U5OrXL7TXjonEKuEHwRhzz1JA8
7LXm/wENwn1/AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAuxJxHCGOw9DgNtw4wqi78OE3djdiLwbie+2CevKMRaO14IhuQGVK
w1PYsnTuVLVcJl3Y4QKQ4nnbe1QCiGrLq9wueQy7ZvBeZry3f+QD1Q/PAG19n6/7
hlhXclSOJ/jRah0Gi+QXAycKE5RES/Qn4F5fNE7MxzM0ZQHIlszZLNUrcpeLE9nX
avlqlSqK8FmLPOpOSRrdPtzKP2sjW9UUFVGMfurDYIC51hkZI/nyy8A1C844sfuF
LV6oYpYw5+soA122zBqGqP6vApwFCvWSDcGlx8xj1Irdo+JIDfK8vklu9P11rTWB
R7dZw9pD21reD0pf0Bipzneho6iiL++w+QIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
aMzjdOHri8Kmdoy0qt1a33Y9/e5vKkJQkzCKdHN34Il5FMMCkOrJ1yeQgZyp6mU4
jPSpUZlr1Iq52x5ers4fH4SybvX16BDq+p6+Zel9f5TpFg1vzdpJH1WOJ3ZoES1N
S8CpiXVz8flc5Ez6Dc7uZGSE2fYRl1Pswn3GuLfR1Wjw0VNp1VgHZk6xYXRk/YLx
OyjZTWEWAF/0qw3usXtvTvh6wGniVxr0rg3zZbesLXti4TAn3B3N6VG1TPOizna6
s26edpQ6RQPigAuccEwU5iaIQEGkIxcoe61qnPvAoWP3Jk/sZAGCqhbya0CBCH8U
pEW/OauwlDlr3yXEKh05aQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
XhAoF04YrM2zJUvrQuEsGhU53Pbf1B0jv5F4YkMlRX2y15rKXKI93vQTM1LbnYc0
ETkhSOQB2rpnX0bcE+K+x0sWXiMRtR1HSX/oIPDI9MNqHv75eZlEkSaDJHIsQJlj
Dd++tMHkRc49nNNo2J25J3TiBU0ecpVYYvtJzynE3W8tX3io6EmvTehkj2o79z0A
ax2A5JG65plch0ES2yK2jqgBEmkA/eZENDNQAaERXMFJbbpHIMBaGguwCEieJe77
JBAOxJFRGpL6MhMpcvi5MgEMqfAv3AhlBo93n4apT2CYR8PdCHUZyq7FrgwTSJS7
ndl3YmvxJ7wnyTXitw0GcSVeQaYMQV+LR9Z1VkmjIwRuHliUn7hR79pYqs3t11aQ
muW8jOrx+5QsiTLEPV6Hs0pzXc9XDw7mnJ6M2gxxF8fZCztal3TNLs9+1O22fxME
0VU1oS7SG6T4M1YOXgKFUP20gLl8sZf+3lGp3aLZIK8psR3vzggpaRSUKgip4Lqv
-----END SIGNATURE-----
dir-key-certificate-version 3
dir-address 154.35.175.225:80
fingerprint EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97
dir-key-published 2021-02-08 18:16:07
dir-key-expires 2022-02-08 18:16:07
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAwBmqdD+G0q3smN5OBFHCcK5pQH5G1GIpFJ1JxCVEp92tTK4ZHnot
9RzMfag6zQFqwLaJ+yEb1DOjTdTMfcUTsj5f3GUqPB+U7shSMAvvAAM+Bx/4m1AU
u6sk4XmPB1bCBfcRl4zhnY6XFIbj0ktuBDblcxHz3lDgHFpBoci9sF59mM14MZ09
EdwgeckcU5oeq6ApuSlUVaOT8xsKV/yeK4SKaFfDclwPAJuitQ5CpqctP7ExmlrY
sboTDtz7/Xa6OccaGDEUf7TRlipvUX6rvlmvHm3qjdixVfExpa8E5QG79GZTL82p
1zBd3iqc6QEnRDTiW9cMUeQt4EvrwOUVVYPWo3hp1C/iiNzWraDays2xuhaSB0gj
fPatu2CFW5XB2vd9IvIiWeklSFqnF8DL38jDL7DbFiETJreGsDMR03yHWVd0MbPz
OrvAxG4tJn+JtnwhzlbRjnfk53jOTbiM0vMV8h/ztapCiJeT/6i7nVQ1xL2boeYw
5RDUlwZaQiaXAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEApIIcKBWvD0P2YQtsrFKEF1kprJUCEUlWqzV4mVbTcVdzVQpct8t8
NAO8kDbxRSyU2S6gKecusy4H1MJWVAe2qvKIY974espuJwBXWFgT70jSBTFzjMpB
dAaTTY+kNZa66kjBjCVolr8UfFvL7HaL3CCtWD9ds7+ep76co1h3s3sD2BWW/M5m
V6ML8kYkjRW6SW8YHW6By3G+UuqRiGziJIIwQAoPnNSWrzW6UTLpVRDjdo70bQvU
vvfppUuNNji5SFfzSiakxHIse/eHG/rTNSzOvlpjuZxzPIcekr71eu1hCVHb2QdA
9Ikc5pUQeB0zImI8WJ9OVJDFUEgjJ9LGtQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
cy+VPbSGSJ5aI7egCwgNY6mgSlJumULFmUN8gfahvMo5hUwVLqP1FtoKIO8yBUc0
Y47pt6G5a0fjm6mjapFbU7IpqIUl+5gLBRKD6ugx+hr2IoqIVJY7WQUvVMBnfqHp
Z5N6kXfFBT+EbnbLiUqoRo1/AHC6E6CqI5pdhV86UCFydmuLf/MfwJpXiYRJueqk
DnPYEflq+Zu/RReL5aJlVOVuWq0ZpuzUHk4gIicKESLGkv4eI2CvuB5HTeNAB9L5
laMe+YpoXqgqMae1HT+rupPXYeONPygFXXbNLNVrR7OjAYG2TOaqdUTQkFefFVtD
ungKyPS6LTytSuU/rjWCbQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
sV4ouMb8pmCM3WjLTFxfRVb6qZl8rQ0tYM/AjKz1ufU0UmL7yx/7JMg2InDcYPH3
4PIUQrDLoAMxnnNVMDaDGoGDGI5keUmU1eSGvdJYN7okd0aRvA9LFGw4uDVVyB0K
l7BOk80y15P34R4G6oPvcR8aCxoiMh9UusbhiVjBr6dAfJGVVxZAO1ZJ8pa8fcrA
IbtNks2vut6Oy4oaC7zLCwcbRJM6dSvzcbzBpCf7/b9w6NQNqCBBQkqKgUl0FqKM
QRKqHWuhbqcL9+lj7rvgWCEigLu9ff1+E7C4BV7GzOm5FPcRqfkPaMsjQuM/HErH
swhf2Ra+Tcdk9gdI4AomqwaoD6B2uKsZkcFpZhq4HAle6rOP9eC16DpqsokpqoW9
vb5Mic7ABYVpB4t3o5wOI9D4exXmzv6gpuOyl5rJGL5ORYSEhnMGsKMyPceCGysg
SzwfPWBqRTM2LfBxhW05UEBJev4EXk7AA5sr6GkcX/CXeR47pyXQAyc2doZo7Aoq
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 49015F787433103580E3B66A1707A00E60F2D15B
dir-key-published 2021-11-28 16:32:54
dir-key-expires 2022-02-28 16:32:54
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAxVbS0noZKz1Ei6858RGyyuQgwQUKG4Urrp2BiAzkYxwX+6fURlut
AjeLb4XysqCdNdUipuLRQ2QIy1C220QiCHV6jZAsM4tmEq6TpK6q1lxi5YPKqbGS
CfUQFT1nO4s4DCYSLCwiRNy6bMe8tNHc0MpXP3loCbPkYCoXrEL6vYIROw3oeGWE
KbFPQrzYJAPHgUubBibsY5lkUY9N/5QZw2y1bn+dq9mFOoCIHLd6DkQmySmftnMe
QrpYA2WvE4M5yN2HB8QGT7TdzXPPL6889rFw/mjqYExQPX7cqmILkchsB7I5whjA
u0oodF8Y9ooK9QT0GeK4h3xQhzNG17anuUxbZ7sxzmBwBNmkNyLWEeIntazyjRFr
P2mDY/9YK2JOQKkh3tKl1whcCG9ZtAhKmm/ijG7OrhqtusdGKBXIgALf4f111AK1
gNcacDx2fJzRHuNK8zkIORAzStxKdLbAbBNeLENk1zBjSkrxCOJH4mBpr8TXULq1
ThLI/8OzZq4LAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAo32l4qg46cqP/sAL+oLmQM0mDiQUy6EtNa73vyy0BJEGWJeImUO4
gHNg9pyMFqyF+rP824gAzwX9Un9HaKgFpIrsKcZzg+Yl2vlrBQpJ0NPIkN9oqj27
W/A7RftMhH2itv0v87QudD7FqJpxdYNf3wpr9GvsAiHZMBfC88WhCnmJoDBwyucY
HFH7gzjPeDx37KD57o2M1KC/SRVtQtrccA/WzcxNypgAYkJu4yE2gaDr2WFn3hFv
kscW0jn6+157UuKH0rCNeRFDx8SsSS0nr6Zk/n+dlXzHGDO3vQIKCoRoH9yL4T//
hkMYE/4qc9R49VyXxK+n/qU6HQYpQMi+VwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
KKrOMRAg1bx+XFLRjhQB5OFjtupkqkFnGjS3LZQ5FHNwJ7cKG1X7K0aJNAumRPMD
w9xraIMuuok46wj35P9Tcy68qi3EqIJP5378ZtdK3Ncy9KkSWJSA9MLPmB3fClL/
/5TwboePXRdlt++Bcw9OC83HDuFVBqZArFIvopKf/AJOyViRVHlBmgNKFpm9RJTo
XsD415rJAi79tAfXzKuagke6DTVqobMhxrUmp3RjbEEEC6icQ3YX9X6NOPQ4Gwl2
bpWOVi3/9EGRge0X8IYsqB6/pnEXM2FSOTMdwo4YQzIgW/HLE9hXjFCx7QcPkcos
AZHvl12tKzZF3F9MKPcNyQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
wb40bfNLWyU9pWW+2BAHbrmZZfbs9UEIS/6G66VE6823/r6M90RCmFx1JlwqgmaV
/WMbBE9DxFKILyhuQi6whIKoYndg72VDeZL5BzWctalw24VpJePVjeGLcTdJHBVh
a6UiQFaQdH+bTX6qNIFv9nNwq7ZzJRBvaYF9bK8kaTrZilFKoRVBxXssBUFjEz6t
f+sei5WIiBnzaQOUxdMjvdDAHci4DXwGw1U2M7jcYARo4FfvWkAxzWLxocWmauPM
8tDn0fSgMnLlSOR2crnriQMFhYD+9xyxfOq1IDH2IWCKlejz7j3DHSqBYiUSO9oD
uX6htwbMWwZQeqt+LttE/zZX1Tcv6PJqemT8uabH0s94W2A3sJpstWJ+0capb+Mj
bvTXj7t2ilqa5RX35KKhaQ6wlh4OXZb2ydeJZc7wtyG8eN53aVqJNJQ+WZn4IiTq
fefr2ojy2VDJLDHJVNpKQQzmjXtSs+69wCvrqdHGjGAQl5L31LjZgaNLNj14RI+H
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint D586D18309DED4CD6D57C18FDB97EFA96D330566
dir-key-published 2021-09-06 18:42:41
dir-key-expires 2022-09-06 18:42:41
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAvi5+A+XPw4jxMYhmEI4+MpnaX3dUEbsMGHA+xAMnmVhuxbm3Dn5c
TyhQNY2LOlsieE84UYG+J4dABfaFH4w0l6zUJkuytX4+6WRQontw9puR/IcXkRwM
8Tv/tY675OYRCm9DgDAWfqZM0IgTzSrYRDl8eFPSFCOP0NhMrQZeUrdKgwAXVZWP
xt9nTCwT4K9BMp47LEmZKdEokeVsr0l29Z9v5+r24k9x8EQjDexsoHwlVrxWfarG
1klWssfSFpkMN+FkTQnBC6ByiBh5ZKM5AC/HkVFvuHjehUpfrtNk6XNFcKbDvEIg
qPdg1QWuuSWpZVA+/EwSBtwMNcq9pv60L8Cm9WCJoSC691WByiGwFCy1/XcBI4J/
BkoMEvP3kAxzm92jqGbpFSJawFRPZKy89FDKpha/So3CERQPV0ar+DTpVqDlryWV
N4x1IzpPeSHFj7T74q8qdrxx0wcAjWJ9WYoGQif6FK3hHcmbSGSgyvAFeoYxyUCL
JHkjBCD4WTWVAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA1Hguh3PNTfMd7kLD8NupSdye1KB6hhQitx8DipvT71ZaCZYI8fZ3
z5xa6fCcJXv/xoz1tzKeJ1n4/AzAbc7ltCyyWkj9CbiB99cEe+sVE9R899eFvPP9
DUmsmgy0Bn2MrdfD/N5VeJ219TTtqI75BJMd9n1+r5zUzhji2ihcLWYgi0GVZoec
6B+xfPtYbifCdrPRBwrMAW4EhtMKeJfzsYFO220f7x2OmmZB9muesi5O8/0zjwu1
xOKldXCFbccTfFN88nYmaO8j0SpG9nOveFXavPs0LyVzhuMkbLXSWAN+M/S6GC4L
1kbkjQ6YhuYSnKxGFo/wdax41jrSFCf3qQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2a4uvuN2LUgSuHoWYckJusjzeaEfTTN+DHJdQjJrMueZtxhhz+C+soYoSAvBsHI
huc2y0wLbeNMpLXeyGoYh0M4zm6RyjMksHQZbgPjkibflbUI3csJcvaBMQonfo9B
knoqOzeQd8NioOlnbYQ+k78swGtg2ndHpK4G4NMBK6ZQrbhrIk0nVhOhaIHpGdiN
icr+czGq6SzH4Snp26dJ+J+9SAdTOzgat/C2Othdu122JR2/7GzCnz8dqS3LabN9
iWJWMLxayFKi+Z5f1WjCNOVh5lSjpeLjUNSrA1hXXKSRD0eFOQFRvgvO60gyvooH
C8amqpSD8HqsCK6MvN7V9g==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
uuQhm7YYbqR4/fGSDWqzwiw40nr7y3laCmfiTDHOA5N6lVOw/tM4bdMbGH58wkkK
XBIEz4zQnIT4Sgaumc9PZK3/a8wkx3HgvSNZAEvv5GK2iD8QQNaR0mv7/gbCOLeD
4MAsWH7ehc2u0AcebYehYWE7/fknYRfIGLRzeAeR479LgtFIaaZ54lGeEWKA7qBc
B3njJcNDlekChydxw2JRMw2GmK7Gn/cVRLjFiG32aaTPA37Ietw6Z8wXEjTy4087
KTzTe6puX0g9kCWMaGIBzod+ucNOG9WhgVfy6M+OMddI4KbgizUM7a5c3DZwnQHk
nn5yqib/W7NmHZOL1k2qYlKQlbr412bsDgBDoFYSYPIkbO4x7LHJnGGiwxYx4vmx
caxDySQtqCcR9ygMrZVrL9W/Z+w2N/KCXnL+SgTmN0x/Saor1ZTkONj5Tfn4dg/W
xDxvLO02DpVTfgidUsBeHSnMQn7w0iG0abhWFmYNFDjxZFEWy30mRCEYADC/1NCI
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint ED03BB616EB2F60BEC80151114BB25CEF515B226
dir-key-published 2021-03-29 03:27:58
dir-key-expires 2022-03-29 03:27:58
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA1d6uTRiqdMp4BHBYIHKR6NB599Z1Bqw4TbOVkM2N1aSA4V/L/hKI
nl6m/2LL/UAS+E3NCFX0dhw2+D7r7BTJyfGwz0H2MR6Py5/rCMAnPl20wCjXk2qY
ACQa0rJvIqXobwGnDlvxn4ezsj0IEY/FEb61zHnnPHf6d3uyFR1QT06qEOQyYzML
76f/Lud8MUt+8KzsdnadAPL8okNvcS/nqa2bWbbGhC8S8rtDpPg5BhX2ikXa88RM
QdrrackdppB2ttHlq9+iH3c8Wyp7bvdH8uhv410W7RnIE4P+KIxt3L0gqkxCjjyh
mn9ONcdgNOKe31q2cdW5LOPSIK+I5/VTjYjICza7Euyg03drpoBMGLuuJZY6FXEV
auIBncWe+So8FMxqU/fwo5xm6x085U1MwXUmi4XDYpr/kau6ytPnzzw9J++4W9iC
em5Jp0vaxrDnPdphqT0FWsBAwsZFL7nZRnmUlTgGsXUa0oSM9/MErDwzELh/NwG4
DNyyzRG8iP61AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAsw2ZJlGsmfDmDwoKbzjOno759Xwqn6JX+tFasI8eRjOFnOyjYzd1
XjG6Gj2hVpF/ze2NiTuUyRu3Ybp8G9/gs8VGPljxSHkEugGjQdYFoST02ma0vUHA
8YqpBYOiLvsXnqfEkl3Yj6HVxmVJA9j8BxODODlBtxRMJWFrpp/b+qCo/YyGmCh3
n0qd3QNqFPLIzwvjWVhaFfga8dXBT73wX9uYT7nT/e3pV7ZvTw0caqi7svNzj0I8
/OxOEjoBQEQMQVPT2bNZKBe9X8QKDSgdealZQwBT9wdZ4KndtCj6Y8MVjj15/YtH
fWfNyUHgVqOmfDK7m3pHXR9fGgsLQexIfQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
GyN9MMaPABXJ16WAFIhdzOhNT59BI0RAfV9ZpUJgzkAAmpoj+WwUtWfcrW7a08CT
9g60QwJonP/Nh+8iLvQYH5ZyEqsEj0HEUD/yI2kvN41Y5QBD1Sku8Cu4E2UaICzL
V63oitjQzppKlVXHyP/SXsI2bUjoHLtT2pBvxRJ84DlZBEQ/ZqS38NN/+Z6DtMR/
kn0l7W5yA3bYWzeKy1TeKLWo7p0hHzv/Hswe/eha+27LuwZZwwfSQrRy1fi66Fmj
0xBP+iXXtmNleFegFuhEBPXa+9udrT9rodSdazkGPzjyF6HWRMP5DtmTI6ovJDVX
60UQ0hNb6KAP+FZKPz9/dA==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
zAgmCR7tf0btsogvBmdxJ7+RWjPBzmDSA7f+zlK0jOc3lDDD4pxuQO6iNcoEDYMC
5hkzVoGBMYsxLfLZXFGE518dn79iKr6SQoq87AhnAsIiAfXMk2AWLkWI7MTzZo2U
dcgo+7vzxMObk86vzFxYWrSqp6CSZ7RwBRnH3vCGLfOMZ1lXMug+MQAQYAyl6KIR
3d/uEu3+sPFJcHQFP3C/7bHDG5j/76kwoFzjSjg974rSjr1j1FbrpNn35mLc+2X/
11n7cOADHWaSN3MlLWGsYxuuX2l1w/XZNfFEezDwK3BOotbj5spU2nQ8xbDFPB5+
ixDfc00TC3YbveSz+S8W9czfysJt3KaWmQczDtSIXag1qrL52CBGUVGP6+R7xnpR
/4QD6yCKmDcNk2D1YnindwYC48ydDt/u9A/97cEBpUbul3feW7eKLk79MIklWlWo
3c3aQVH6Ewrb76oXYYwzNbqJOp2ceREu72/Fk/keprVcupVDtVoqHgDDpfOUYTJd
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4
dir-key-published 2021-09-01 00:00:00
dir-key-expires 2022-03-01 00:00:00
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA7cZXvDRxfjDYtr9/9UsQ852+6cmHMr8VVh8GkLwbq3RzqjkULwQ2
R9mFvG4FnqMcMKXi62rYYA3fZL1afhT804cpvyp/D3dPM8QxW88fafFAgIFP4LiD
0JYjnF8cva5qZ0nzlWnMXLb32IXSvsGSE2FRyAV0YN9a6k967LSgCfUnZ+IKMezW
1vhL9YK4QIfsDowgtVsavg63GzGmA7JvZmn77+/J5wKz11vGr7Wttf8XABbH2taX
O9j/KGBOX2OKhoF3mXfZSmUO2dV9NMwtkJ7zD///Ny6sfApWV6kVP4O9TdG3bAsl
+fHCoCKgF/jAAWzh6VckQTOPzQZaH5aMWfXrDlzFWg17MjonI+bBTD2Ex2pHczzJ
bN7coDMRH2SuOXv8wFf27KdUxZ/GcrXSRGzlRLygxqlripUanjVGN2JvrVQVr0kz
pjNjiZl2z8ZyZ5d4zQuBi074JPGgx62xAstP37v1mPw14sIWfLgY16ewYuS5bCxV
lyS28jsPht9VAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA+jzmadukj4Q0qLgJ0at+nDXGruO5JD3HsehobiwO8HrdaaImY+rY
CZzxRWM4xryQ2AFuAGbSxGoNQT3dTLvjKNVdGY6jPzlS7vxKbPeNZtc/YMvfZ+Fx
uEjvaZ6nDbviVtQhtE0J2EZ32n90Ob8YC8l/7zh0hp+mZO6Wf2DGXWjNvG7d8Ucc
p5A1ZVIpJ/VQzdlPaocO+6AvxvSBpaIUF0yGpTwofTOjtUmZyuWbhRndsQj1qMcj
e8wzOIgr3HZyhO9wztQGkZ8bzHq65oZe0IIOXZu0icZamFGQ5I6y5duCqxDDe4C/
v1/6bD1I+/ujLXRMmkcbJ3NZE+KrZg7KIE5ScGbkJIX7vIicqtsf+7VipdOh3/wp
qaDxX9Sp2cbVUU0M/aJ14nDSeFlx0XQAgWkPjG2lYtTNEC2zuudBCuCD8es8EhAW
FrU94cYg9lVId0NDMOpWPMH2QJFS4tk3Hc66si3+gkCOt2GOaSQeD+gGWkdwDzn3
S8iAur2GohFFAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
B4914rKqUc51Q1nq8CrA/e7EaMQ0ug08qlBqWyzZSDiBKVCoQj446ZJMU9VKlzJF
XtuURlJ7zswXMze7HceakrkxZAc7GiAGKO5hgbbI8XGLvXn16Lsr/MP1cmbKoI04
g5tG9Kx6yOB4r/l2TQY9Tw22YcdJ24W2/mw6TmDv0b+IorsIBnxIDv7Q7j25IkNE
hW3F9R+Ntja1RWPqKnptp8nxBt5/2jVr637BFczDv9K509QX+HHKyICA1hnvDDU7
N5Y1/mVu4JwQrBAFL857XbobP4QaLsZ34Q8LRE4dveuyw+vjVa1YimZ6h/RvrYyP
8DUi4XnzFyztecivXbdSTpMTSMfC4NQXFeT+XStRdWlapZyCFhp74w3wv7HCB0z6
7QT1HWMKPRvj1DsHhvPviyLVCL2tl2x+G7aaledOPf6BbhO7VolNeHiubyYCQl2H
t/Vy72DZbQeuLhf5GyqVyUm9uugzvVrryUiNUApOW8Xta2dAEBqinDrrY6iMYxh/
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
UrJN3Ey1hSHTaMUAhINCVFhojt48ppxky0bvwztQ9p/Vy7dfRx0APNbL70/XZOrR
sRj8zxtx2+tc5Lnkfaah63bmVsUNTgD6LudDaffXiV8XhIeVbzS0r/YJ0U1OsbK+
ApDItNDUz+VIJL5JUDjq/6fojFlWPYNIwyk5G8zOM70Atjk6UDyCIihV2u5pofW3
znFaFp/XhC14S8lMPZYKbnyl2iQ7UsqLpTxg3EwivIlSVFs5YQe0yXgJFX0oNd9Z
gAf3JIonA2g8Oo9EkgRfYCI33AwyVoU3QN1/AmLH2uPWTKhMu7k+OHktuIBfyFTR
9jbUq+YTU1ni6kEsJVBP/0I4n9Xb4VYIoqOq0BrcEp3lQ8BCEWjIGwLh1HYc9/DY
meE+cwLp0RNU8cuxyrGnkLA350bsNxrDkiaHAkj5ZA8W9VTGYsBxVhbLdQzN3GOm
63GJBgjdaOsD6WXs/737nD2sLu6dnA/Jbz84ouZSafQO/FNQZnndfj4osjabmq8O
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 27102BC123E7AF1D4741AE047E160C91ADC76B21
dir-key-published 2021-08-21 23:12:45
dir-key-expires 2022-08-21 23:12:45
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAuxgnMVH4vwBjMeGvrEODOYcjbCS4N+Wt0SZ6XA5I08HyMf5AbaaF
MDscJBRIUOp7DyLmUwK+jp+QI8pUjjKsB8S0ctb/J3Im2T6CXnP2KgEfVmpNVQmV
XdMm8cRZl1uIZDDBAXizSQ51f9A17TJh7pF/5khYp/SAzl6aO5ETn7ry0ITiJnNa
6cY+400F7ZBA8NuXnCHVGfmpFFsiJKFrS1Kve629eeaNEd3mynRviBXJy5a4NEGf
y42Ev8on6SxEnF9OG0NMJ081/+mP+j8Dsl3+Uehzr9B42MQQfDo4RdYGrt9XolBm
L4eay1ieZEsFeDy0TMfiGGbr90wo1fgGLHIRSfTNLhhPJ/f9cTZPe98rhSgGWiAd
RvK5SljoIOR4qdS9/aiZkj1P+etvh1rIQUcG4/xCOBnouEBK+DDHZFqyMtpMPtV0
Bxi20DVaMJcyhdfjVqcRSyuR8tlOnTid6QwBj6kgIIfMaC+4Ht6yO/SYquCWlaZl
y7Pu7li8WyW9AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAu1QJ+qlCbtrYsg9QENtOpvMrEDc+WgH1ZFxEqk0v/ad2ACQYe4dT
g1gJ6VZyGths3p8+WQXIA9YbcEr2oajXlLmLT2QAlqlsXMPKwwIpeG4rvR43Wwq5
mQ7aX+/VjZ9YZNoZVEAy1O7ti7GIXJzJYDOdgBjLifSjpjcEmSaf+v4E357azs9R
ndLHRRwbBLdUl7G3aMkL4ejrv6AAXexPxAL90xsb/MAhVEOQrJNcVMTgII0fSf56
P1J17SQwthNZ4rTMo2O9TvWUGNf1sMb9kdm+A2Nwo2CKmUR0uo5wHN4YHSBYFDcb
hxRtZlhSFfBJvJgrX8/+CnJrBFC6S/7vJwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
VzdxGiA6egLcZjm3hHrTW+N+7sPwaEcdYR5/GW757MO79O8QwjOLJSa5dOF5yDWa
3Ie+QDB2Q++a3+w776zqpFsaGCuEHBCfU9pxatKdoTsExZdQnWHJg4u9YD1JcYvL
dZq9uCCWaKa91OjA6/U9zp/LY3tOPUWCyO4MHehHYggzapbcF5uaMG0AT0lZzaXJ
vh180N5YGMfm0eYXJqkJyX3WCZhGroh7o3DyaqtBSJ1cY4NzTNgPoCAi3J/XEoCI
3JzxfH97uyqQngR1yGb5KggdM2ejci3Ld9q99hjXlzYRtsobUQBTlZca2vUk5ALh
vFWU7GzcNIdDjKBUd+IhTQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
WldVJc0L3fByDY9D+Yha2/8Bw5nqRpHu8RL8lU8zXb0rhfiA6DN2aefSCG9WEay1
sTVs4zhS8N0+278oP51Lg7S+V6SmXxKZbF3I2zNaFMIPVspRA3OF0R9vCP1YMxeo
D4EDQXiRxNNeM9hAjmmLiE0j7ZMetZ88ewwsOOXAvPHmCth09nWdCYKwibwUW/U3
w1HXCHK4BdZ7XXkMJNry7kZP/H1/5oYyuKshue4+f8tgRa0xg0wbHDrdbrR1qBBu
Xg9Q11hV29RIdD0ZgugUdyGKlpRa6Mb3GD5DzN845sFhCjsPrYyZv7Xo/S+uTtN4
t59BtF1TjnsAhUk7shk2W3zzaauzUarkDA1v+mb9NZ3CFsNgfxt6d1yDDyuxJgaC
VkIfO9nX9dCORSC9Ow+XOq+D/o4FfSe4q0VvrJJILAcw2Nu1Zg4ZhHBeugP9f4I9
p/+ZMrEr+YAKkjPw+uIEdR0l/YZEhcrjymX4FY829LNll2AceqMCKoapFWuEzzDP
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 0232AF901C31A04EE9848595AF9BB7620D4C5B2E
dir-key-published 2021-08-01 20:00:02
dir-key-expires 2022-08-01 20:00:02
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu9O0Pueesn0+29BlxZs60mBqehjdQtgSnKOm9QZxbQ0xrMQgbFnR
hWbKD8erenyeFk2SF6AJkbyzgYC89hyPW+8GBDmg5bE8fRKjgV/nI3tY2m4rkY3u
zSmYIdwqHUUc98Xzt9PaQ8IJAlDBY4XLKrWmJMxSyhBlVEept7+9Tj23qowW44Mz
xPJZ1aFkB1FpkD6qmoCzVZbhXy3cGt1nDwdJK7KqlaXziz9pFiw8PzTVU2xFgJNy
+nEcT72DBtk3G5K2Riu/aXY/D541Cioj9KMV4Nv4g8aBKx58Xq2tq1pFkc1Bqj1y
2MomVR3iskFzlqC8yKWGVe4OP2IaOhtcQJYp5GR9q+dWnr53WWNVxNu3sA9iMal3
PJUk5pIYrsmArGew5gmlCe+Al46nPINxc7ouztmStAV+2F6SpZlKOcstnT+KJ52O
1xnOSaj/WnzG2o4KZ9UrFQoUNOLQJcelPcC+vrinMk9BQPcB072l9NjpUBC9brsW
qTCMStn1jfDDAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAp7nHn/R+ZZ8lza379M7BJ00JYPAcncjtoa2K2Z75bDoxlegGvZXp
j4D0WhqksaaOr/+YCSPMcs4HAapKE/Dj09p1kjzh6Xu/iVp51NiQAARS5j3tu/5k
WJQ7ig207TdtjmslZIx0UU8pieuenRdyUN0PvjOkaoZIpao1+UlIe47DP+42D3QX
1J2wu48QDvt7hUUA3y7yLUyNMarqYBbbXQ/MpH8tcMT76TTN1uilP6W/3j1b6Fr7
NGtbUrS1EzOOHnCpgpnD8qGcisDKrHcVkNkh1w+8LW9ef7RGpFPpn022hUQG0WLD
5zrh19SAsKetWAZY6RlvyCHPVReajIAovwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
ca3I8mHu2zEOCnzySzdk+rbZLpohw5aa3NmTGFzRUXqOeHClOYHRc+glAyCrtUA3
lEa5fiFaZTImKu0J/uroyR4uF5JpzLOfojTQi9P5hMCBSdd7uGzoKC+/dKb2OngZ
VkBjptMf1S9dy2lUdDksHnnyg8UrV7EolIHUFNdEBI1LeONkdesZ5oQMg3HRlVpU
v+m/7y/MB+o3KAXkQyAxTcV4bKdsHm3Pf0CSfDgOPImwFS4lwyEW0STlOmVHojZR
5wm+5dwt9vbD7K6ectbnWtWjiSrvtGjqixO652lxz1qrsid99S5wEzJNhfif8lYe
VsB9h7YagNHJHLiGeBT1kg==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
ZkHpe8JVvGsiAsH8gw1eZVIIE8WuM+3Sdd37U2tOyDi7FVwJV+oJ+aKwcCTqTLaj
jglQJbg2JdV4ofy49ZaQa6FBGLrzxAS6Gx0jg+28Kzbr0xu9hSX81oPSXKn9KDLr
BvmuSqKBB+5B9nIEBjm6FwPc8MjqlvNesuJ3IpW9+e85eB7qsH4ozjHF0GIgpXu/
qXrk2TEK1nMc9EN+VCYuy3gAm46GHQEYR1U7gIofCYf7LQpDrfj1sAGquCQ3vYqT
Ex3GtqcDV22IME67Cou5rv9OmMnmy1dbeHO4g843RX0LXtEDdGYGSLHzl8EAscrg
i55XFlS6z5OwCbdDvFTkHUWRlaiDtoymaxAEW6GUmNjHhgWY9wJwgroVNRsP8Ihi
aex9HIND1MY4ERS41Csba/0grf+FahMVI12gwpmrnKfF95QHWw2MEvT1pzZGtMnq
XD8mcVNYJtcTvYM/cUa0I4BFD1AyeIP54hEXwIsqHm8KBJpjX/ZpPzksnc4NY8i0
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 23D15D965BC35114467363C165C4F724B64B4F66
dir-key-published 2021-09-18 16:07:20
dir-key-expires 2022-09-18 16:07:20
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAlv6XS+VppPaQzOgor0YFlcXLWeXiMn5N3VBneXuw8maLOu9oPJ9z
2/oMQN8a+VOWTf+/jebGzOBK6MamXpgsIZPQWiT18gZMsYdR8mcqBYqVP3khwUWh
9QYkV+m+Auxa0TLzTrsi6dLDJ384XdpDweU+YJghMJNZ1NqiT8ogj84hxs5Tf+Qf
bn7EBIcU7SAKr5Lw25KrMb5e3AZSC5MilBS/KLgVTq/GiWb7pKd5pxGwlGolNX8a
PccZ2ZT2DrSQsct4wVxhSbUqANI3PfMpXvmUDxWWBgbQwLF02/4gi+13snlHtqwl
y1WjE55HVfx1CTX13SStwmF/N3SFtFf1qil3j5qrHdHtKlAYOaTfqab1eLVH1l83
LI5QWD7ri9GpPqIjlh6PuaHjaO2FW20SouZtS9jJKwi1l1G3ef1tSlha1cxkRxIp
U/ngvQBsoa9X26VfQA4MieZgVVdMVwjCNh2YC9aEXc/KxfcBueZkM1194qP88cVu
dOFYaftOkuGPAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA3OigVlkOvlx54wcY0RRuExNw2sPYHr8m8QP+SnzARDbrubvsKT0S
z/+aVWccgacBtihOpF9juQLHb+nqhea9s6QS8XAQ98bqm8foKToWuxnIRS9c+8e1
qcENTfh6U2Dr8ckwVcKAPtnLnPtbxuFF5UiqXAPA89ZmtqUPv+DfmDr5fdeb0bCu
Lo6TCFLQOcn2Qz1WsSv/2JRkSBy8pgaC01zErgv9oRVIzFfLn8YpfnWZkFiRGwX6
/GBLsS19SLLX0xLkPwQ/CwN6OkipOtYi6UNq0osHw9xfm5sCzcnltJShA1YtIp72
e1HkTx03a43uAKlJBo1rMD29stVJu9ABEwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2YFJIj1zR9iMPWRJYMDEKuLlV0Gbis9232Gog5sS06LpUFPYL6clLDf7eWAimPs
u8rUUP4JEjmAY7bWHyqbG3D5iljNin50W7kvY8ip+Vqf64vjNUXFDKUbi0iGkfVC
nfX67FL0JF74hqtCtMlS5QPvD4oLsC40DdmPD4kCulaSrMlmsFRGFdl60HeSLbeP
oopRA4yYB4ZGJxJUaSuMm6RrcK08G2l7vLfHpxhcJWQVb2fKB7Ds+AogZYnc6ZYF
hpGAP9y+Yn8TUUqPMhhZwLw/8eUAhtv8G2aBBxHyctlGvg1YFiquPP6VEn88h9GZ
X4d/mLOAQeYWEalQC812iw==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
OlLQuEDdagECnVy1Nke/C7dpS8+8XvgLK/hGgV/OpCWr+Gq8bL6/NpK9GP7EbsUI
NxsguJ2r1wkEGTixz88gdKWDCC3evGW1pqnsjkCk69gHGtCxmrFeiCxCSomaOjzw
HCnp3TcT1DA4EstoXUqPysVkBYkx8OBO6rGhvE+G1S4bVG/EJkPCMhjPlxX41ON6
NWvtf32dviA5W1BrYKWJy/v0pCsApgjZa6qpaLdgqQabG5YEJA0rONS1hL+AcJks
CnvkSS7iU/4jrDPfgOLoVxEkH68swLol2Qf0RFHg12EL5kf0xbPnfE84aQyt99an
6VAMVIy/tCxR2efZ0+uQmQk7S35uQH/PxZ3/mq6cDMw7+WZdYbrkyfmFK/A+yL2P
op71Ik0Xf0Qwd0qMhTZMVPZpZDQmxvr0j0r3xHia0Ez+PhovnnxqI9/cThRQ/ceN
jE2cA46H4ZfYn5OdCP+mP9L+MsqJYoHj/SigcIrXUX58R1D0JWVX2KPU+tVyQ03B
-----END SIGNATURE-----

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
# Tor state file last generated on 2022-02-04 13:57:02 local time
# Other times below are in UTC
# You *do not* need to edit this file.
CircuitBuildTimeBin 875 1
CircuitBuildTimeBin 1025 1
CircuitBuildTimeBin 1125 1
CircuitBuildTimeBin 1175 1
CircuitBuildTimeBin 1275 1
CircuitBuildTimeBin 1475 1
CircuitBuildTimeBin 1875 1
CircuitBuildTimeBin 1925 1
CircuitBuildTimeBin 2025 1
CircuitBuildTimeBin 2375 1
Dormant 0
Guard in=default rsa_id=D70A5E01EC14D078164D5E587608949F85FD771B nickname=leaders2 sampled_on=2022-01-30T18:34:14 sampled_idx=0 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-02-02T06:04:33 confirmed_idx=1 pb_circ_attempts=13.000000 pb_circ_successes=10.000000 pb_successful_circuits_closed=10.000000 pb_timeouts=1.000000
Guard in=default rsa_id=598C8E4E92D8D7750991D23F6776EE92EC1A00C1 nickname=Unnamed sampled_on=2022-02-01T01:24:07 sampled_idx=1 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-01-29T15:09:32 confirmed_idx=2
Guard in=default rsa_id=013ABAED8F4CDB677BE0B5212E4B2583B86035EE nickname=b0rken sampled_on=2022-01-27T22:32:07 sampled_idx=2 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-02-03T10:23:27 confirmed_idx=0
Guard in=default rsa_id=66DFC51E724E38DDCB3AB67DDF4C0BB0F1F49C0B nickname=Seccom04 sampled_on=2022-01-26T06:52:32 sampled_idx=3 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=9400AF52EC2929DA41E6DDD3B684F23643BC316C nickname=BM03 sampled_on=2022-01-30T16:35:42 sampled_idx=4 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=DD0C8EEC5CA402A9FA4478F10C31A440F71F6885 nickname=chaosDelroth sampled_on=2022-01-31T18:30:35 sampled_idx=5 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=6055FE90C18DD4B2593A9D0E02DDC4D68E9BA62E nickname=elects2 sampled_on=2022-01-29T10:24:36 sampled_idx=6 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=79233E8202A0EE755B39BC24BE80939C023B3FCD nickname=m83fr2 sampled_on=2022-02-01T17:58:48 sampled_idx=7 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=C4BDBFD1049EE1D4F6977C42485F94CD5F97BA92 nickname=lenin sampled_on=2022-01-29T15:57:51 sampled_idx=8 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=876C5AC1D2811E650AD4C78B77841C1ACB3B0088 nickname=whoUSicebeer05b sampled_on=2022-01-29T15:26:49 sampled_idx=9 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=FF8B7CAD5F508972509D79F933FB24D2F524AB75 nickname=einNettesRelay sampled_on=2022-02-02T09:24:01 sampled_idx=10 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=BFB14B9ADF1F19E59258CAE7FCDBAAC9937B7B10 nickname=Yggdrasil sampled_on=2022-02-02T13:42:48 sampled_idx=11 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=ACBD72F9395DE8DE293D37CCF7733F1BE23EDA53 nickname=hopUSicebeer7b sampled_on=2022-02-02T18:42:20 sampled_idx=12 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=F79370BA46ADC03CC10866924EE4A3C470BAFE93 nickname=skankhunt42de4 sampled_on=2022-01-25T11:00:17 sampled_idx=13 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=C1939D36649DE98A202429631D8EFC70128D5F5F nickname=rinderwahnRelay10L sampled_on=2022-01-26T01:10:26 sampled_idx=14 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=3BC258C20BBB8094C12DA7CB982B36D1502FAC3C nickname=Assange041us sampled_on=2022-01-31T23:57:37 sampled_idx=15 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=B2242163F681F77F93190663AC5F696ACF76C4FD nickname=torexit42 sampled_on=2022-02-03T12:50:47 sampled_idx=16 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=1F953ACBFB9F44CE38543B7E9C0E0BE1BDC7E941 nickname=bauruine sampled_on=2022-01-24T00:30:24 sampled_idx=17 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=F00C8B7589FEE52BE84387CDB422C1F1386979C0 nickname=JennyRelay sampled_on=2022-01-30T03:59:39 sampled_idx=18 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=CD39C258265B25EAA4ABA4FDCB2DF98104CAA362 nickname=normaray sampled_on=2022-01-30T09:12:11 sampled_idx=19 sampled_by=0.4.5.9 listed=1
LastWritten 2022-02-04 21:57:02
MinutesSinceUserActivity 3
TorVersion Tor 0.4.5.9 (git-d0ed04d50e80fe1c)
TotalBuildTimes 10

View File

@ -0,0 +1,3 @@
SocksPort 9735 OnionTrafficOnly
ControlPort 9736
HashedControlPassword 16:178423CBFA6B25BD608424EE12A1FACF5D36FF43215636A5917F8D64DF

View File

@ -0,0 +1,2 @@
DÍk­HzöĺN(Ăĺ•őŐ`x<>ć—×ë#€ÍěĎ3— c˛2_Ś3‰őťŞRC6~-zSÉĘ?JŽčĐ<>4 fřŔăhŢýüv˝F†8áBü1Q\˛"lHh5í§ <¦‹;$J3č"źúBamT5 <Á4îě•Żcj™ÚŽśËgˇŹÉÂ/<2F>‰vęĽnKÄßVG†Ű~YdŞG ÝrQPÁrôŕ<C3B4>ljěăČ-ZĽy÷_Ľ¸˙˛M·ôÄĽĂ!AX^HR˘¦ç<>őX ;ăŘ©Ô)@2šŔ”tŕÍŁä ý´[cdX®ĹĹ­+ý,0wa}ß%<25><11>
Ď9˘x[ÝČNP]Ĺ5o_M@Ą†™±!©•Ű0:IÄŮóv€ó;ź ÔĂP¨Öe?€˛Śip*‰{<ŐČ—´,RyMnď-‡gËťëĺÉfŹÚV<56>Dá>łĐ;S8őĎ0l¦k "ľÖöĘ1˝żA4ţ<11>vG6î€Uş^"zÓ«H¸´e”S•<53>°u\čč=Ś5ńë¨Ů¤»]aď˝3ąďí©g`î-SŻŢŰ…ý#ăE÷× ¸=Śµ˘W,8­đdŚĚä.˙ Ž҉=şPüÖ•Ąń_<C584>jäĎřĚvyěž,Ľî<˙xokV»`<60>vQ|Ć/©˙ťs˝0şę-tÖ-ßĂ[şa=ý­ôßć<>ŕé÷µ×łëöN­řmďV!qŘŹZ†Т˛Ł Ě˛±saŢSúT ‚ż€”ôîěuDąL<>­-‘Ç"šŇíRşŁŰ<*'ófgߧČM~ŇŚ"sŐ<73>âl/xuĹ<řsÂÄ4÷~Ůf>yś§­NÝ{ľ¨b!Ey 'ˇězęoZ!×<>\"´a(Ýp,PhŔrZ…ŐMµ-…ë+ĐľÇĆą ÉŚťÎĘÝô\}ë~ł?sJL|ú©z˙·°IaXYh/á ą2;vą“u§…ÉĽ<C389>P€(

BIN
integration_test/env/persist/dev/SALT vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,411 @@
dir-key-certificate-version 3
fingerprint 14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4
dir-key-published 2021-09-01 00:00:00
dir-key-expires 2022-03-01 00:00:00
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA7cZXvDRxfjDYtr9/9UsQ852+6cmHMr8VVh8GkLwbq3RzqjkULwQ2
R9mFvG4FnqMcMKXi62rYYA3fZL1afhT804cpvyp/D3dPM8QxW88fafFAgIFP4LiD
0JYjnF8cva5qZ0nzlWnMXLb32IXSvsGSE2FRyAV0YN9a6k967LSgCfUnZ+IKMezW
1vhL9YK4QIfsDowgtVsavg63GzGmA7JvZmn77+/J5wKz11vGr7Wttf8XABbH2taX
O9j/KGBOX2OKhoF3mXfZSmUO2dV9NMwtkJ7zD///Ny6sfApWV6kVP4O9TdG3bAsl
+fHCoCKgF/jAAWzh6VckQTOPzQZaH5aMWfXrDlzFWg17MjonI+bBTD2Ex2pHczzJ
bN7coDMRH2SuOXv8wFf27KdUxZ/GcrXSRGzlRLygxqlripUanjVGN2JvrVQVr0kz
pjNjiZl2z8ZyZ5d4zQuBi074JPGgx62xAstP37v1mPw14sIWfLgY16ewYuS5bCxV
lyS28jsPht9VAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA+jzmadukj4Q0qLgJ0at+nDXGruO5JD3HsehobiwO8HrdaaImY+rY
CZzxRWM4xryQ2AFuAGbSxGoNQT3dTLvjKNVdGY6jPzlS7vxKbPeNZtc/YMvfZ+Fx
uEjvaZ6nDbviVtQhtE0J2EZ32n90Ob8YC8l/7zh0hp+mZO6Wf2DGXWjNvG7d8Ucc
p5A1ZVIpJ/VQzdlPaocO+6AvxvSBpaIUF0yGpTwofTOjtUmZyuWbhRndsQj1qMcj
e8wzOIgr3HZyhO9wztQGkZ8bzHq65oZe0IIOXZu0icZamFGQ5I6y5duCqxDDe4C/
v1/6bD1I+/ujLXRMmkcbJ3NZE+KrZg7KIE5ScGbkJIX7vIicqtsf+7VipdOh3/wp
qaDxX9Sp2cbVUU0M/aJ14nDSeFlx0XQAgWkPjG2lYtTNEC2zuudBCuCD8es8EhAW
FrU94cYg9lVId0NDMOpWPMH2QJFS4tk3Hc66si3+gkCOt2GOaSQeD+gGWkdwDzn3
S8iAur2GohFFAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
B4914rKqUc51Q1nq8CrA/e7EaMQ0ug08qlBqWyzZSDiBKVCoQj446ZJMU9VKlzJF
XtuURlJ7zswXMze7HceakrkxZAc7GiAGKO5hgbbI8XGLvXn16Lsr/MP1cmbKoI04
g5tG9Kx6yOB4r/l2TQY9Tw22YcdJ24W2/mw6TmDv0b+IorsIBnxIDv7Q7j25IkNE
hW3F9R+Ntja1RWPqKnptp8nxBt5/2jVr637BFczDv9K509QX+HHKyICA1hnvDDU7
N5Y1/mVu4JwQrBAFL857XbobP4QaLsZ34Q8LRE4dveuyw+vjVa1YimZ6h/RvrYyP
8DUi4XnzFyztecivXbdSTpMTSMfC4NQXFeT+XStRdWlapZyCFhp74w3wv7HCB0z6
7QT1HWMKPRvj1DsHhvPviyLVCL2tl2x+G7aaledOPf6BbhO7VolNeHiubyYCQl2H
t/Vy72DZbQeuLhf5GyqVyUm9uugzvVrryUiNUApOW8Xta2dAEBqinDrrY6iMYxh/
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
UrJN3Ey1hSHTaMUAhINCVFhojt48ppxky0bvwztQ9p/Vy7dfRx0APNbL70/XZOrR
sRj8zxtx2+tc5Lnkfaah63bmVsUNTgD6LudDaffXiV8XhIeVbzS0r/YJ0U1OsbK+
ApDItNDUz+VIJL5JUDjq/6fojFlWPYNIwyk5G8zOM70Atjk6UDyCIihV2u5pofW3
znFaFp/XhC14S8lMPZYKbnyl2iQ7UsqLpTxg3EwivIlSVFs5YQe0yXgJFX0oNd9Z
gAf3JIonA2g8Oo9EkgRfYCI33AwyVoU3QN1/AmLH2uPWTKhMu7k+OHktuIBfyFTR
9jbUq+YTU1ni6kEsJVBP/0I4n9Xb4VYIoqOq0BrcEp3lQ8BCEWjIGwLh1HYc9/DY
meE+cwLp0RNU8cuxyrGnkLA350bsNxrDkiaHAkj5ZA8W9VTGYsBxVhbLdQzN3GOm
63GJBgjdaOsD6WXs/737nD2sLu6dnA/Jbz84ouZSafQO/FNQZnndfj4osjabmq8O
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint D586D18309DED4CD6D57C18FDB97EFA96D330566
dir-key-published 2021-09-06 18:42:41
dir-key-expires 2022-09-06 18:42:41
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAvi5+A+XPw4jxMYhmEI4+MpnaX3dUEbsMGHA+xAMnmVhuxbm3Dn5c
TyhQNY2LOlsieE84UYG+J4dABfaFH4w0l6zUJkuytX4+6WRQontw9puR/IcXkRwM
8Tv/tY675OYRCm9DgDAWfqZM0IgTzSrYRDl8eFPSFCOP0NhMrQZeUrdKgwAXVZWP
xt9nTCwT4K9BMp47LEmZKdEokeVsr0l29Z9v5+r24k9x8EQjDexsoHwlVrxWfarG
1klWssfSFpkMN+FkTQnBC6ByiBh5ZKM5AC/HkVFvuHjehUpfrtNk6XNFcKbDvEIg
qPdg1QWuuSWpZVA+/EwSBtwMNcq9pv60L8Cm9WCJoSC691WByiGwFCy1/XcBI4J/
BkoMEvP3kAxzm92jqGbpFSJawFRPZKy89FDKpha/So3CERQPV0ar+DTpVqDlryWV
N4x1IzpPeSHFj7T74q8qdrxx0wcAjWJ9WYoGQif6FK3hHcmbSGSgyvAFeoYxyUCL
JHkjBCD4WTWVAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA1Hguh3PNTfMd7kLD8NupSdye1KB6hhQitx8DipvT71ZaCZYI8fZ3
z5xa6fCcJXv/xoz1tzKeJ1n4/AzAbc7ltCyyWkj9CbiB99cEe+sVE9R899eFvPP9
DUmsmgy0Bn2MrdfD/N5VeJ219TTtqI75BJMd9n1+r5zUzhji2ihcLWYgi0GVZoec
6B+xfPtYbifCdrPRBwrMAW4EhtMKeJfzsYFO220f7x2OmmZB9muesi5O8/0zjwu1
xOKldXCFbccTfFN88nYmaO8j0SpG9nOveFXavPs0LyVzhuMkbLXSWAN+M/S6GC4L
1kbkjQ6YhuYSnKxGFo/wdax41jrSFCf3qQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2a4uvuN2LUgSuHoWYckJusjzeaEfTTN+DHJdQjJrMueZtxhhz+C+soYoSAvBsHI
huc2y0wLbeNMpLXeyGoYh0M4zm6RyjMksHQZbgPjkibflbUI3csJcvaBMQonfo9B
knoqOzeQd8NioOlnbYQ+k78swGtg2ndHpK4G4NMBK6ZQrbhrIk0nVhOhaIHpGdiN
icr+czGq6SzH4Snp26dJ+J+9SAdTOzgat/C2Othdu122JR2/7GzCnz8dqS3LabN9
iWJWMLxayFKi+Z5f1WjCNOVh5lSjpeLjUNSrA1hXXKSRD0eFOQFRvgvO60gyvooH
C8amqpSD8HqsCK6MvN7V9g==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
uuQhm7YYbqR4/fGSDWqzwiw40nr7y3laCmfiTDHOA5N6lVOw/tM4bdMbGH58wkkK
XBIEz4zQnIT4Sgaumc9PZK3/a8wkx3HgvSNZAEvv5GK2iD8QQNaR0mv7/gbCOLeD
4MAsWH7ehc2u0AcebYehYWE7/fknYRfIGLRzeAeR479LgtFIaaZ54lGeEWKA7qBc
B3njJcNDlekChydxw2JRMw2GmK7Gn/cVRLjFiG32aaTPA37Ietw6Z8wXEjTy4087
KTzTe6puX0g9kCWMaGIBzod+ucNOG9WhgVfy6M+OMddI4KbgizUM7a5c3DZwnQHk
nn5yqib/W7NmHZOL1k2qYlKQlbr412bsDgBDoFYSYPIkbO4x7LHJnGGiwxYx4vmx
caxDySQtqCcR9ygMrZVrL9W/Z+w2N/KCXnL+SgTmN0x/Saor1ZTkONj5Tfn4dg/W
xDxvLO02DpVTfgidUsBeHSnMQn7w0iG0abhWFmYNFDjxZFEWy30mRCEYADC/1NCI
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 49015F787433103580E3B66A1707A00E60F2D15B
dir-key-published 2021-11-28 16:32:54
dir-key-expires 2022-02-28 16:32:54
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAxVbS0noZKz1Ei6858RGyyuQgwQUKG4Urrp2BiAzkYxwX+6fURlut
AjeLb4XysqCdNdUipuLRQ2QIy1C220QiCHV6jZAsM4tmEq6TpK6q1lxi5YPKqbGS
CfUQFT1nO4s4DCYSLCwiRNy6bMe8tNHc0MpXP3loCbPkYCoXrEL6vYIROw3oeGWE
KbFPQrzYJAPHgUubBibsY5lkUY9N/5QZw2y1bn+dq9mFOoCIHLd6DkQmySmftnMe
QrpYA2WvE4M5yN2HB8QGT7TdzXPPL6889rFw/mjqYExQPX7cqmILkchsB7I5whjA
u0oodF8Y9ooK9QT0GeK4h3xQhzNG17anuUxbZ7sxzmBwBNmkNyLWEeIntazyjRFr
P2mDY/9YK2JOQKkh3tKl1whcCG9ZtAhKmm/ijG7OrhqtusdGKBXIgALf4f111AK1
gNcacDx2fJzRHuNK8zkIORAzStxKdLbAbBNeLENk1zBjSkrxCOJH4mBpr8TXULq1
ThLI/8OzZq4LAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAo32l4qg46cqP/sAL+oLmQM0mDiQUy6EtNa73vyy0BJEGWJeImUO4
gHNg9pyMFqyF+rP824gAzwX9Un9HaKgFpIrsKcZzg+Yl2vlrBQpJ0NPIkN9oqj27
W/A7RftMhH2itv0v87QudD7FqJpxdYNf3wpr9GvsAiHZMBfC88WhCnmJoDBwyucY
HFH7gzjPeDx37KD57o2M1KC/SRVtQtrccA/WzcxNypgAYkJu4yE2gaDr2WFn3hFv
kscW0jn6+157UuKH0rCNeRFDx8SsSS0nr6Zk/n+dlXzHGDO3vQIKCoRoH9yL4T//
hkMYE/4qc9R49VyXxK+n/qU6HQYpQMi+VwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
KKrOMRAg1bx+XFLRjhQB5OFjtupkqkFnGjS3LZQ5FHNwJ7cKG1X7K0aJNAumRPMD
w9xraIMuuok46wj35P9Tcy68qi3EqIJP5378ZtdK3Ncy9KkSWJSA9MLPmB3fClL/
/5TwboePXRdlt++Bcw9OC83HDuFVBqZArFIvopKf/AJOyViRVHlBmgNKFpm9RJTo
XsD415rJAi79tAfXzKuagke6DTVqobMhxrUmp3RjbEEEC6icQ3YX9X6NOPQ4Gwl2
bpWOVi3/9EGRge0X8IYsqB6/pnEXM2FSOTMdwo4YQzIgW/HLE9hXjFCx7QcPkcos
AZHvl12tKzZF3F9MKPcNyQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
wb40bfNLWyU9pWW+2BAHbrmZZfbs9UEIS/6G66VE6823/r6M90RCmFx1JlwqgmaV
/WMbBE9DxFKILyhuQi6whIKoYndg72VDeZL5BzWctalw24VpJePVjeGLcTdJHBVh
a6UiQFaQdH+bTX6qNIFv9nNwq7ZzJRBvaYF9bK8kaTrZilFKoRVBxXssBUFjEz6t
f+sei5WIiBnzaQOUxdMjvdDAHci4DXwGw1U2M7jcYARo4FfvWkAxzWLxocWmauPM
8tDn0fSgMnLlSOR2crnriQMFhYD+9xyxfOq1IDH2IWCKlejz7j3DHSqBYiUSO9oD
uX6htwbMWwZQeqt+LttE/zZX1Tcv6PJqemT8uabH0s94W2A3sJpstWJ+0capb+Mj
bvTXj7t2ilqa5RX35KKhaQ6wlh4OXZb2ydeJZc7wtyG8eN53aVqJNJQ+WZn4IiTq
fefr2ojy2VDJLDHJVNpKQQzmjXtSs+69wCvrqdHGjGAQl5L31LjZgaNLNj14RI+H
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 27102BC123E7AF1D4741AE047E160C91ADC76B21
dir-key-published 2021-08-21 23:12:45
dir-key-expires 2022-08-21 23:12:45
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAuxgnMVH4vwBjMeGvrEODOYcjbCS4N+Wt0SZ6XA5I08HyMf5AbaaF
MDscJBRIUOp7DyLmUwK+jp+QI8pUjjKsB8S0ctb/J3Im2T6CXnP2KgEfVmpNVQmV
XdMm8cRZl1uIZDDBAXizSQ51f9A17TJh7pF/5khYp/SAzl6aO5ETn7ry0ITiJnNa
6cY+400F7ZBA8NuXnCHVGfmpFFsiJKFrS1Kve629eeaNEd3mynRviBXJy5a4NEGf
y42Ev8on6SxEnF9OG0NMJ081/+mP+j8Dsl3+Uehzr9B42MQQfDo4RdYGrt9XolBm
L4eay1ieZEsFeDy0TMfiGGbr90wo1fgGLHIRSfTNLhhPJ/f9cTZPe98rhSgGWiAd
RvK5SljoIOR4qdS9/aiZkj1P+etvh1rIQUcG4/xCOBnouEBK+DDHZFqyMtpMPtV0
Bxi20DVaMJcyhdfjVqcRSyuR8tlOnTid6QwBj6kgIIfMaC+4Ht6yO/SYquCWlaZl
y7Pu7li8WyW9AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAu1QJ+qlCbtrYsg9QENtOpvMrEDc+WgH1ZFxEqk0v/ad2ACQYe4dT
g1gJ6VZyGths3p8+WQXIA9YbcEr2oajXlLmLT2QAlqlsXMPKwwIpeG4rvR43Wwq5
mQ7aX+/VjZ9YZNoZVEAy1O7ti7GIXJzJYDOdgBjLifSjpjcEmSaf+v4E357azs9R
ndLHRRwbBLdUl7G3aMkL4ejrv6AAXexPxAL90xsb/MAhVEOQrJNcVMTgII0fSf56
P1J17SQwthNZ4rTMo2O9TvWUGNf1sMb9kdm+A2Nwo2CKmUR0uo5wHN4YHSBYFDcb
hxRtZlhSFfBJvJgrX8/+CnJrBFC6S/7vJwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
VzdxGiA6egLcZjm3hHrTW+N+7sPwaEcdYR5/GW757MO79O8QwjOLJSa5dOF5yDWa
3Ie+QDB2Q++a3+w776zqpFsaGCuEHBCfU9pxatKdoTsExZdQnWHJg4u9YD1JcYvL
dZq9uCCWaKa91OjA6/U9zp/LY3tOPUWCyO4MHehHYggzapbcF5uaMG0AT0lZzaXJ
vh180N5YGMfm0eYXJqkJyX3WCZhGroh7o3DyaqtBSJ1cY4NzTNgPoCAi3J/XEoCI
3JzxfH97uyqQngR1yGb5KggdM2ejci3Ld9q99hjXlzYRtsobUQBTlZca2vUk5ALh
vFWU7GzcNIdDjKBUd+IhTQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
WldVJc0L3fByDY9D+Yha2/8Bw5nqRpHu8RL8lU8zXb0rhfiA6DN2aefSCG9WEay1
sTVs4zhS8N0+278oP51Lg7S+V6SmXxKZbF3I2zNaFMIPVspRA3OF0R9vCP1YMxeo
D4EDQXiRxNNeM9hAjmmLiE0j7ZMetZ88ewwsOOXAvPHmCth09nWdCYKwibwUW/U3
w1HXCHK4BdZ7XXkMJNry7kZP/H1/5oYyuKshue4+f8tgRa0xg0wbHDrdbrR1qBBu
Xg9Q11hV29RIdD0ZgugUdyGKlpRa6Mb3GD5DzN845sFhCjsPrYyZv7Xo/S+uTtN4
t59BtF1TjnsAhUk7shk2W3zzaauzUarkDA1v+mb9NZ3CFsNgfxt6d1yDDyuxJgaC
VkIfO9nX9dCORSC9Ow+XOq+D/o4FfSe4q0VvrJJILAcw2Nu1Zg4ZhHBeugP9f4I9
p/+ZMrEr+YAKkjPw+uIEdR0l/YZEhcrjymX4FY829LNll2AceqMCKoapFWuEzzDP
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint ED03BB616EB2F60BEC80151114BB25CEF515B226
dir-key-published 2021-03-29 03:27:58
dir-key-expires 2022-03-29 03:27:58
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEA1d6uTRiqdMp4BHBYIHKR6NB599Z1Bqw4TbOVkM2N1aSA4V/L/hKI
nl6m/2LL/UAS+E3NCFX0dhw2+D7r7BTJyfGwz0H2MR6Py5/rCMAnPl20wCjXk2qY
ACQa0rJvIqXobwGnDlvxn4ezsj0IEY/FEb61zHnnPHf6d3uyFR1QT06qEOQyYzML
76f/Lud8MUt+8KzsdnadAPL8okNvcS/nqa2bWbbGhC8S8rtDpPg5BhX2ikXa88RM
QdrrackdppB2ttHlq9+iH3c8Wyp7bvdH8uhv410W7RnIE4P+KIxt3L0gqkxCjjyh
mn9ONcdgNOKe31q2cdW5LOPSIK+I5/VTjYjICza7Euyg03drpoBMGLuuJZY6FXEV
auIBncWe+So8FMxqU/fwo5xm6x085U1MwXUmi4XDYpr/kau6ytPnzzw9J++4W9iC
em5Jp0vaxrDnPdphqT0FWsBAwsZFL7nZRnmUlTgGsXUa0oSM9/MErDwzELh/NwG4
DNyyzRG8iP61AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAsw2ZJlGsmfDmDwoKbzjOno759Xwqn6JX+tFasI8eRjOFnOyjYzd1
XjG6Gj2hVpF/ze2NiTuUyRu3Ybp8G9/gs8VGPljxSHkEugGjQdYFoST02ma0vUHA
8YqpBYOiLvsXnqfEkl3Yj6HVxmVJA9j8BxODODlBtxRMJWFrpp/b+qCo/YyGmCh3
n0qd3QNqFPLIzwvjWVhaFfga8dXBT73wX9uYT7nT/e3pV7ZvTw0caqi7svNzj0I8
/OxOEjoBQEQMQVPT2bNZKBe9X8QKDSgdealZQwBT9wdZ4KndtCj6Y8MVjj15/YtH
fWfNyUHgVqOmfDK7m3pHXR9fGgsLQexIfQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
GyN9MMaPABXJ16WAFIhdzOhNT59BI0RAfV9ZpUJgzkAAmpoj+WwUtWfcrW7a08CT
9g60QwJonP/Nh+8iLvQYH5ZyEqsEj0HEUD/yI2kvN41Y5QBD1Sku8Cu4E2UaICzL
V63oitjQzppKlVXHyP/SXsI2bUjoHLtT2pBvxRJ84DlZBEQ/ZqS38NN/+Z6DtMR/
kn0l7W5yA3bYWzeKy1TeKLWo7p0hHzv/Hswe/eha+27LuwZZwwfSQrRy1fi66Fmj
0xBP+iXXtmNleFegFuhEBPXa+9udrT9rodSdazkGPzjyF6HWRMP5DtmTI6ovJDVX
60UQ0hNb6KAP+FZKPz9/dA==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
zAgmCR7tf0btsogvBmdxJ7+RWjPBzmDSA7f+zlK0jOc3lDDD4pxuQO6iNcoEDYMC
5hkzVoGBMYsxLfLZXFGE518dn79iKr6SQoq87AhnAsIiAfXMk2AWLkWI7MTzZo2U
dcgo+7vzxMObk86vzFxYWrSqp6CSZ7RwBRnH3vCGLfOMZ1lXMug+MQAQYAyl6KIR
3d/uEu3+sPFJcHQFP3C/7bHDG5j/76kwoFzjSjg974rSjr1j1FbrpNn35mLc+2X/
11n7cOADHWaSN3MlLWGsYxuuX2l1w/XZNfFEezDwK3BOotbj5spU2nQ8xbDFPB5+
ixDfc00TC3YbveSz+S8W9czfysJt3KaWmQczDtSIXag1qrL52CBGUVGP6+R7xnpR
/4QD6yCKmDcNk2D1YnindwYC48ydDt/u9A/97cEBpUbul3feW7eKLk79MIklWlWo
3c3aQVH6Ewrb76oXYYwzNbqJOp2ceREu72/Fk/keprVcupVDtVoqHgDDpfOUYTJd
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 23D15D965BC35114467363C165C4F724B64B4F66
dir-key-published 2021-09-18 16:07:20
dir-key-expires 2022-09-18 16:07:20
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAlv6XS+VppPaQzOgor0YFlcXLWeXiMn5N3VBneXuw8maLOu9oPJ9z
2/oMQN8a+VOWTf+/jebGzOBK6MamXpgsIZPQWiT18gZMsYdR8mcqBYqVP3khwUWh
9QYkV+m+Auxa0TLzTrsi6dLDJ384XdpDweU+YJghMJNZ1NqiT8ogj84hxs5Tf+Qf
bn7EBIcU7SAKr5Lw25KrMb5e3AZSC5MilBS/KLgVTq/GiWb7pKd5pxGwlGolNX8a
PccZ2ZT2DrSQsct4wVxhSbUqANI3PfMpXvmUDxWWBgbQwLF02/4gi+13snlHtqwl
y1WjE55HVfx1CTX13SStwmF/N3SFtFf1qil3j5qrHdHtKlAYOaTfqab1eLVH1l83
LI5QWD7ri9GpPqIjlh6PuaHjaO2FW20SouZtS9jJKwi1l1G3ef1tSlha1cxkRxIp
U/ngvQBsoa9X26VfQA4MieZgVVdMVwjCNh2YC9aEXc/KxfcBueZkM1194qP88cVu
dOFYaftOkuGPAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA3OigVlkOvlx54wcY0RRuExNw2sPYHr8m8QP+SnzARDbrubvsKT0S
z/+aVWccgacBtihOpF9juQLHb+nqhea9s6QS8XAQ98bqm8foKToWuxnIRS9c+8e1
qcENTfh6U2Dr8ckwVcKAPtnLnPtbxuFF5UiqXAPA89ZmtqUPv+DfmDr5fdeb0bCu
Lo6TCFLQOcn2Qz1WsSv/2JRkSBy8pgaC01zErgv9oRVIzFfLn8YpfnWZkFiRGwX6
/GBLsS19SLLX0xLkPwQ/CwN6OkipOtYi6UNq0osHw9xfm5sCzcnltJShA1YtIp72
e1HkTx03a43uAKlJBo1rMD29stVJu9ABEwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
r2YFJIj1zR9iMPWRJYMDEKuLlV0Gbis9232Gog5sS06LpUFPYL6clLDf7eWAimPs
u8rUUP4JEjmAY7bWHyqbG3D5iljNin50W7kvY8ip+Vqf64vjNUXFDKUbi0iGkfVC
nfX67FL0JF74hqtCtMlS5QPvD4oLsC40DdmPD4kCulaSrMlmsFRGFdl60HeSLbeP
oopRA4yYB4ZGJxJUaSuMm6RrcK08G2l7vLfHpxhcJWQVb2fKB7Ds+AogZYnc6ZYF
hpGAP9y+Yn8TUUqPMhhZwLw/8eUAhtv8G2aBBxHyctlGvg1YFiquPP6VEn88h9GZ
X4d/mLOAQeYWEalQC812iw==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
OlLQuEDdagECnVy1Nke/C7dpS8+8XvgLK/hGgV/OpCWr+Gq8bL6/NpK9GP7EbsUI
NxsguJ2r1wkEGTixz88gdKWDCC3evGW1pqnsjkCk69gHGtCxmrFeiCxCSomaOjzw
HCnp3TcT1DA4EstoXUqPysVkBYkx8OBO6rGhvE+G1S4bVG/EJkPCMhjPlxX41ON6
NWvtf32dviA5W1BrYKWJy/v0pCsApgjZa6qpaLdgqQabG5YEJA0rONS1hL+AcJks
CnvkSS7iU/4jrDPfgOLoVxEkH68swLol2Qf0RFHg12EL5kf0xbPnfE84aQyt99an
6VAMVIy/tCxR2efZ0+uQmQk7S35uQH/PxZ3/mq6cDMw7+WZdYbrkyfmFK/A+yL2P
op71Ik0Xf0Qwd0qMhTZMVPZpZDQmxvr0j0r3xHia0Ez+PhovnnxqI9/cThRQ/ceN
jE2cA46H4ZfYn5OdCP+mP9L+MsqJYoHj/SigcIrXUX58R1D0JWVX2KPU+tVyQ03B
-----END SIGNATURE-----
dir-key-certificate-version 3
dir-address 154.35.175.225:80
fingerprint EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97
dir-key-published 2021-02-08 18:16:07
dir-key-expires 2022-02-08 18:16:07
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAwBmqdD+G0q3smN5OBFHCcK5pQH5G1GIpFJ1JxCVEp92tTK4ZHnot
9RzMfag6zQFqwLaJ+yEb1DOjTdTMfcUTsj5f3GUqPB+U7shSMAvvAAM+Bx/4m1AU
u6sk4XmPB1bCBfcRl4zhnY6XFIbj0ktuBDblcxHz3lDgHFpBoci9sF59mM14MZ09
EdwgeckcU5oeq6ApuSlUVaOT8xsKV/yeK4SKaFfDclwPAJuitQ5CpqctP7ExmlrY
sboTDtz7/Xa6OccaGDEUf7TRlipvUX6rvlmvHm3qjdixVfExpa8E5QG79GZTL82p
1zBd3iqc6QEnRDTiW9cMUeQt4EvrwOUVVYPWo3hp1C/iiNzWraDays2xuhaSB0gj
fPatu2CFW5XB2vd9IvIiWeklSFqnF8DL38jDL7DbFiETJreGsDMR03yHWVd0MbPz
OrvAxG4tJn+JtnwhzlbRjnfk53jOTbiM0vMV8h/ztapCiJeT/6i7nVQ1xL2boeYw
5RDUlwZaQiaXAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEApIIcKBWvD0P2YQtsrFKEF1kprJUCEUlWqzV4mVbTcVdzVQpct8t8
NAO8kDbxRSyU2S6gKecusy4H1MJWVAe2qvKIY974espuJwBXWFgT70jSBTFzjMpB
dAaTTY+kNZa66kjBjCVolr8UfFvL7HaL3CCtWD9ds7+ep76co1h3s3sD2BWW/M5m
V6ML8kYkjRW6SW8YHW6By3G+UuqRiGziJIIwQAoPnNSWrzW6UTLpVRDjdo70bQvU
vvfppUuNNji5SFfzSiakxHIse/eHG/rTNSzOvlpjuZxzPIcekr71eu1hCVHb2QdA
9Ikc5pUQeB0zImI8WJ9OVJDFUEgjJ9LGtQIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
cy+VPbSGSJ5aI7egCwgNY6mgSlJumULFmUN8gfahvMo5hUwVLqP1FtoKIO8yBUc0
Y47pt6G5a0fjm6mjapFbU7IpqIUl+5gLBRKD6ugx+hr2IoqIVJY7WQUvVMBnfqHp
Z5N6kXfFBT+EbnbLiUqoRo1/AHC6E6CqI5pdhV86UCFydmuLf/MfwJpXiYRJueqk
DnPYEflq+Zu/RReL5aJlVOVuWq0ZpuzUHk4gIicKESLGkv4eI2CvuB5HTeNAB9L5
laMe+YpoXqgqMae1HT+rupPXYeONPygFXXbNLNVrR7OjAYG2TOaqdUTQkFefFVtD
ungKyPS6LTytSuU/rjWCbQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
sV4ouMb8pmCM3WjLTFxfRVb6qZl8rQ0tYM/AjKz1ufU0UmL7yx/7JMg2InDcYPH3
4PIUQrDLoAMxnnNVMDaDGoGDGI5keUmU1eSGvdJYN7okd0aRvA9LFGw4uDVVyB0K
l7BOk80y15P34R4G6oPvcR8aCxoiMh9UusbhiVjBr6dAfJGVVxZAO1ZJ8pa8fcrA
IbtNks2vut6Oy4oaC7zLCwcbRJM6dSvzcbzBpCf7/b9w6NQNqCBBQkqKgUl0FqKM
QRKqHWuhbqcL9+lj7rvgWCEigLu9ff1+E7C4BV7GzOm5FPcRqfkPaMsjQuM/HErH
swhf2Ra+Tcdk9gdI4AomqwaoD6B2uKsZkcFpZhq4HAle6rOP9eC16DpqsokpqoW9
vb5Mic7ABYVpB4t3o5wOI9D4exXmzv6gpuOyl5rJGL5ORYSEhnMGsKMyPceCGysg
SzwfPWBqRTM2LfBxhW05UEBJev4EXk7AA5sr6GkcX/CXeR47pyXQAyc2doZo7Aoq
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint 0232AF901C31A04EE9848595AF9BB7620D4C5B2E
dir-key-published 2021-08-01 20:00:02
dir-key-expires 2022-08-01 20:00:02
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu9O0Pueesn0+29BlxZs60mBqehjdQtgSnKOm9QZxbQ0xrMQgbFnR
hWbKD8erenyeFk2SF6AJkbyzgYC89hyPW+8GBDmg5bE8fRKjgV/nI3tY2m4rkY3u
zSmYIdwqHUUc98Xzt9PaQ8IJAlDBY4XLKrWmJMxSyhBlVEept7+9Tj23qowW44Mz
xPJZ1aFkB1FpkD6qmoCzVZbhXy3cGt1nDwdJK7KqlaXziz9pFiw8PzTVU2xFgJNy
+nEcT72DBtk3G5K2Riu/aXY/D541Cioj9KMV4Nv4g8aBKx58Xq2tq1pFkc1Bqj1y
2MomVR3iskFzlqC8yKWGVe4OP2IaOhtcQJYp5GR9q+dWnr53WWNVxNu3sA9iMal3
PJUk5pIYrsmArGew5gmlCe+Al46nPINxc7ouztmStAV+2F6SpZlKOcstnT+KJ52O
1xnOSaj/WnzG2o4KZ9UrFQoUNOLQJcelPcC+vrinMk9BQPcB072l9NjpUBC9brsW
qTCMStn1jfDDAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAp7nHn/R+ZZ8lza379M7BJ00JYPAcncjtoa2K2Z75bDoxlegGvZXp
j4D0WhqksaaOr/+YCSPMcs4HAapKE/Dj09p1kjzh6Xu/iVp51NiQAARS5j3tu/5k
WJQ7ig207TdtjmslZIx0UU8pieuenRdyUN0PvjOkaoZIpao1+UlIe47DP+42D3QX
1J2wu48QDvt7hUUA3y7yLUyNMarqYBbbXQ/MpH8tcMT76TTN1uilP6W/3j1b6Fr7
NGtbUrS1EzOOHnCpgpnD8qGcisDKrHcVkNkh1w+8LW9ef7RGpFPpn022hUQG0WLD
5zrh19SAsKetWAZY6RlvyCHPVReajIAovwIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
ca3I8mHu2zEOCnzySzdk+rbZLpohw5aa3NmTGFzRUXqOeHClOYHRc+glAyCrtUA3
lEa5fiFaZTImKu0J/uroyR4uF5JpzLOfojTQi9P5hMCBSdd7uGzoKC+/dKb2OngZ
VkBjptMf1S9dy2lUdDksHnnyg8UrV7EolIHUFNdEBI1LeONkdesZ5oQMg3HRlVpU
v+m/7y/MB+o3KAXkQyAxTcV4bKdsHm3Pf0CSfDgOPImwFS4lwyEW0STlOmVHojZR
5wm+5dwt9vbD7K6ectbnWtWjiSrvtGjqixO652lxz1qrsid99S5wEzJNhfif8lYe
VsB9h7YagNHJHLiGeBT1kg==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
ZkHpe8JVvGsiAsH8gw1eZVIIE8WuM+3Sdd37U2tOyDi7FVwJV+oJ+aKwcCTqTLaj
jglQJbg2JdV4ofy49ZaQa6FBGLrzxAS6Gx0jg+28Kzbr0xu9hSX81oPSXKn9KDLr
BvmuSqKBB+5B9nIEBjm6FwPc8MjqlvNesuJ3IpW9+e85eB7qsH4ozjHF0GIgpXu/
qXrk2TEK1nMc9EN+VCYuy3gAm46GHQEYR1U7gIofCYf7LQpDrfj1sAGquCQ3vYqT
Ex3GtqcDV22IME67Cou5rv9OmMnmy1dbeHO4g843RX0LXtEDdGYGSLHzl8EAscrg
i55XFlS6z5OwCbdDvFTkHUWRlaiDtoymaxAEW6GUmNjHhgWY9wJwgroVNRsP8Ihi
aex9HIND1MY4ERS41Csba/0grf+FahMVI12gwpmrnKfF95QHWw2MEvT1pzZGtMnq
XD8mcVNYJtcTvYM/cUa0I4BFD1AyeIP54hEXwIsqHm8KBJpjX/ZpPzksnc4NY8i0
-----END SIGNATURE-----
dir-key-certificate-version 3
fingerprint E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58
dir-key-published 2021-11-09 19:16:37
dir-key-expires 2023-05-09 19:16:37
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIIBigKCAYEAu/DOrbv/4IAYvyxsy/6ivC3q5yCQBWLKHZGYKQa5G/3rem8wen0f
qF7y4ye6U6faWc5kcNMHEKMIeBzMErxwF345qoGHITxbbOWnizgwPgrdCwlK3p0H
1XZGU/TTjoaM25P+ZNCBvGmDQRAtgs2odnv+i8hpu6vrcAUZYXmmw/Ag1Ou2AlLC
mPpbjV1O5SMylgC4IuCBPr3iA+M1kKkvj4LmwU6pJxjAae76GLzzQ/Ffvi7rRpvU
2BHetjehk+7/t8izgbhT4VABtzKgrv9ATnhfEgPeT/WBq0E75iciBBAXRPF5kEA4
k++NPy21XpL7jkQ4wnMs2HyiFhHbUwbLcoyQ/JVq/WBboSwStYbkdizRpkhJ1eNg
LiD8CPWcZnhWZi9VWrwT0xl+Mu4v6kwo9kVnXhOfcK8Wni9FqiBu2tmNDoGPG1Ac
wptYQSIoujuLgn4MARREwo9cWrKp2w+D7Dt4U7U5OrXL7TXjonEKuEHwRhzz1JA8
7LXm/wENwn1/AgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAuxJxHCGOw9DgNtw4wqi78OE3djdiLwbie+2CevKMRaO14IhuQGVK
w1PYsnTuVLVcJl3Y4QKQ4nnbe1QCiGrLq9wueQy7ZvBeZry3f+QD1Q/PAG19n6/7
hlhXclSOJ/jRah0Gi+QXAycKE5RES/Qn4F5fNE7MxzM0ZQHIlszZLNUrcpeLE9nX
avlqlSqK8FmLPOpOSRrdPtzKP2sjW9UUFVGMfurDYIC51hkZI/nyy8A1C844sfuF
LV6oYpYw5+soA122zBqGqP6vApwFCvWSDcGlx8xj1Irdo+JIDfK8vklu9P11rTWB
R7dZw9pD21reD0pf0Bipzneho6iiL++w+QIDAQAB
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
aMzjdOHri8Kmdoy0qt1a33Y9/e5vKkJQkzCKdHN34Il5FMMCkOrJ1yeQgZyp6mU4
jPSpUZlr1Iq52x5ers4fH4SybvX16BDq+p6+Zel9f5TpFg1vzdpJH1WOJ3ZoES1N
S8CpiXVz8flc5Ez6Dc7uZGSE2fYRl1Pswn3GuLfR1Wjw0VNp1VgHZk6xYXRk/YLx
OyjZTWEWAF/0qw3usXtvTvh6wGniVxr0rg3zZbesLXti4TAn3B3N6VG1TPOizna6
s26edpQ6RQPigAuccEwU5iaIQEGkIxcoe61qnPvAoWP3Jk/sZAGCqhbya0CBCH8U
pEW/OauwlDlr3yXEKh05aQ==
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
XhAoF04YrM2zJUvrQuEsGhU53Pbf1B0jv5F4YkMlRX2y15rKXKI93vQTM1LbnYc0
ETkhSOQB2rpnX0bcE+K+x0sWXiMRtR1HSX/oIPDI9MNqHv75eZlEkSaDJHIsQJlj
Dd++tMHkRc49nNNo2J25J3TiBU0ecpVYYvtJzynE3W8tX3io6EmvTehkj2o79z0A
ax2A5JG65plch0ES2yK2jqgBEmkA/eZENDNQAaERXMFJbbpHIMBaGguwCEieJe77
JBAOxJFRGpL6MhMpcvi5MgEMqfAv3AhlBo93n4apT2CYR8PdCHUZyq7FrgwTSJS7
ndl3YmvxJ7wnyTXitw0GcSVeQaYMQV+LR9Z1VkmjIwRuHliUn7hR79pYqs3t11aQ
muW8jOrx+5QsiTLEPV6Hs0pzXc9XDw7mnJ6M2gxxF8fZCztal3TNLs9+1O22fxME
0VU1oS7SG6T4M1YOXgKFUP20gLl8sZf+3lGp3aLZIK8psR3vzggpaRSUKgip4Lqv
-----END SIGNATURE-----

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,35 @@
# Tor state file last generated on 2022-02-04 13:58:29 local time
# Other times below are in UTC
# You *do not* need to edit this file.
CircuitBuildTimeBin 1075 1
CircuitBuildTimeBin 1175 1
CircuitBuildTimeBin 1325 1
CircuitBuildTimeBin 1675 1
CircuitBuildTimeBin 2125 2
CircuitBuildTimeBin 2175 1
Dormant 0
Guard in=default rsa_id=73283C4DEBC01D3E4A5FD1BB1F2B50D927379F59 nickname=rinderwahnRelay29L sampled_on=2022-01-31T13:54:49 sampled_idx=0 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-01-26T00:18:13 confirmed_idx=2 pb_circ_attempts=7.000000 pb_circ_successes=7.000000 pb_successful_circuits_closed=7.000000
Guard in=default rsa_id=955F15325D6F3E3350EA8A70EB5C49C5BF95C5A0 nickname=XTOMDUS sampled_on=2022-01-31T07:54:08 sampled_idx=1 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-02-01T08:12:17 confirmed_idx=1
Guard in=default rsa_id=82DA9678A0BAE60087AA68A3E1D6E6A2C4246D6D nickname=Ichotolot63 sampled_on=2022-02-03T05:29:22 sampled_idx=2 sampled_by=0.4.5.9 listed=1 confirmed_on=2022-02-02T08:46:50 confirmed_idx=0
Guard in=default rsa_id=77A3ADC5D455778B53C2803761916DFB7DA0A790 nickname=redvader sampled_on=2022-02-02T15:23:39 sampled_idx=3 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=2096BCFEBB95A1134F39FCF8CEB076FF41A2B48B nickname=freja sampled_on=2022-02-03T19:51:55 sampled_idx=4 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=AE4FAE2EB5DC5D078458F0FCBF2B37F5D73F0868 nickname=sinkrlogin sampled_on=2022-01-24T19:30:40 sampled_idx=5 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=680F212ADE23311C658CC560DAF80DB42FEB85DF nickname=a9 sampled_on=2022-01-28T12:54:55 sampled_idx=6 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=3D5D6178C44537E3692853B344385F6572A55767 nickname=TorZabehlice sampled_on=2022-01-31T02:57:52 sampled_idx=7 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=CEDB8AB22915097A3A14F63F28E1E43F86CBECC0 nickname=knight sampled_on=2022-01-26T09:02:31 sampled_idx=8 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=0B555940D37DC849728841C0B290074E1A1BDCA8 nickname=TORro sampled_on=2022-01-26T04:11:45 sampled_idx=9 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=2A6D9EAE2FB319486C5E3BF5CC0F83F06B73CE0F nickname=clicker1 sampled_on=2022-01-28T16:27:29 sampled_idx=10 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=7B3535760987464C8B5686F203B6EBE767C0873E nickname=Hydra69 sampled_on=2022-01-25T23:46:36 sampled_idx=11 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=0F0F690AF1D32C7C3C72C543836625628887BA85 nickname=Hydra57 sampled_on=2022-01-24T15:26:39 sampled_idx=12 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=AC2275831607A95CD8B939C3B8DC466CCF48E2B9 nickname=notsorelay sampled_on=2022-01-25T04:52:58 sampled_idx=13 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=38F732DD349A2E59078434651162A2F4201934A5 nickname=mixminion sampled_on=2022-01-25T18:16:12 sampled_idx=14 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=2F844B6701E6C614240214489A99DF607F43C5B0 nickname=3totalrecovery3 sampled_on=2022-01-31T01:50:15 sampled_idx=15 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=EBE718E1A49EE229071702964F8DB1F318075FF8 nickname=fluxe4 sampled_on=2022-01-24T07:17:01 sampled_idx=16 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=8118F4C0E60C84A453ACCD1641F3A7328CBE6280 nickname=wilfong sampled_on=2022-02-01T10:05:37 sampled_idx=17 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=2AA5F598F9A1812F01CD99E3B59BB87362ED7438 nickname=setsun sampled_on=2022-02-01T12:40:07 sampled_idx=18 sampled_by=0.4.5.9 listed=1
Guard in=default rsa_id=CF6A6080091BB210AA3892FEFE2F6A396DA08DF3 nickname=cryzrelay01 sampled_on=2022-02-01T11:05:41 sampled_idx=19 sampled_by=0.4.5.9 listed=1
LastWritten 2022-02-04 21:58:29
MinutesSinceUserActivity 1
TorVersion Tor 0.4.5.9 (git-d0ed04d50e80fe1c)
TotalBuildTimes 7

View File

@ -0,0 +1,3 @@
SocksPort 9982 OnionTrafficOnly
ControlPort 9983
HashedControlPassword 16:D95A03E7895DC6A2605B7AF7E28AB1E24D813742A0E86A2818C7179CE6

Binary file not shown.

View File

@ -0,0 +1,14 @@
@env:clean
Feature: Splash screen displays and then closes
Scenario: splash screen appears
Then I expect the widget 'SplashView' to be present within 1 second
Scenario: splash screen completes
Then I expect the widget 'ProfileManagerView' to be present within 10 seconds
Scenario: first-run of cwtch creates expected files and folders
Given I expect the widget 'ProfileManagerView' to be present within 10 seconds
Then I expect the folder 'integration_test/env/temp' to exist
And I expect the folder 'integration_test/env/temp/dev' to exist
And I expect the file 'integration_test/env/temp/dev/SALT' to exist
And I expect the file 'integration_test/env/temp/dev/ui.globals' to exist
And I expect the folder 'integration_test/env/temp/dev/tor' to exist
And I expect the file 'integration_test/env/temp/dev/tor/torrc' to exist

View File

@ -0,0 +1,49 @@
@env:persist
Feature: Settings pane opens and can save settings persistently
Scenario: Open the Settings pane
Given I tap the 'OpenSettingsView' button
Then I expect the text 'Cwtch Settings' to be present
And I take a screenshot
Scenario: Change every setting (except Language)
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
When I tap the widget that contains the text "Use Light Themes"
#And I choose option 3 from the "DropdownTheme" dropdown
#When I tap the "DropdownTheme" button
#And I tap the first "ddi_mermaid" element
#And I tap the element that contains the text "Mermaid"
#And I tap the element that contains the text "Mermaid" within the "DropdownTheme"
And I tap the widget that contains the text "Block Unknown Contacts"
And I tap the widget that contains the text "Streamer/Presentation Mode"
And I tap the widget that contains the text "Enable Experiments"
And I tap the widget that contains the text "Enable Group Chat"
And I tap the widget that contains the text "Hosting Servers"
And I tap the widget that contains the text "File Sharing"
And I tap the widget that contains the text "Image Previews and Profile Pictures"
And I fill the "DownloadFolderPicker" field with "/this/is/a/test"
And I tap the widget that contains the text "Enable Clickable Links"
Then I expect the switch that contains the text "Use Light Themes" to be checked
And I expect the switch that contains the text "Block Unknown Contacts" to be checked
And I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
And I expect the switch that contains the text "Enable Experiments" to be checked
And I expect the switch that contains the text "Enable Group Chat" to be checked
And I expect the switch that contains the text "Hosting Servers" to be checked
And I expect the switch that contains the text "File Sharing" to be checked
And I expect the switch that contains the text "Image Previews and Profile Pictures" to be checked
And I expect the "DownloadFolderPicker" to be "/this/is/a/test"
And I expect the switch that contains the text "Enable Clickable Links" to be checked
Scenario: When the app is reloaded, settings from the previous scenario have persisted
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
Then I expect the switch that contains the text "Use Light Themes" to be checked
And I expect the switch that contains the text "Block Unknown Contacts" to be checked
And I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
And I expect the switch that contains the text "Enable Experiments" to be checked
And I expect the switch that contains the text "Enable Group Chat" to be checked
And I expect the switch that contains the text "Hosting Servers" to be checked
And I expect the switch that contains the text "File Sharing" to be checked
And I expect the switch that contains the text "Image Previews and Profile Pictures" to be checked
And I expect the "DownloadFolderPicker" to be "/this/is/a/test"
And I expect the switch that contains the text "Enable Clickable Links" to be checked

View File

@ -0,0 +1,11 @@
Feature: Tor initializes correctly
Scenario: Check the Tor version
Given I tap the icon with type "TorIcon"
Then I expect the Tor version to be present
And I expect the string 'Online' to be present within 60 seconds
Scenario: Reset Tor
Given I tap the icon with type "TorIcon"
Then I expect the string 'Online' to be present within 60 seconds
Then I tap the button with text "Reset"
Then I expect the text "Online" to be absent

View File

@ -0,0 +1,7 @@
Feature: Shutdown Cwtch button works correctly
Scenario: Clicking 'Shutdown Cwtch' shuts down Cwtch
Given I tap the button with tooltip 'Shutdown Cwtch'
Then I expect the text 'Shutdown Cwtch?' to be present
#this also kills the testing framework sadly. will have to find a workaround
#And I tap the button that contains the text 'Shutdown Cwtch'
#Then I wait until the widget with type 'ProfileMgrView' is absent

View File

@ -0,0 +1,14 @@
Feature: Global 'language' setting
Scenario: Change the language to French and back
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
Then I expect the text 'Language' to be present
And I expect the text 'Langue' to be absent
When I tap the widget that contains the text "English"
And I tap the widget that contains the text "Frances"
Then I expect the text 'Langue' to be present
And I expect the text 'Language' to be absent
When I tap the widget that contains the text "Français"
And I tap the widget that contains the text "Anglais"
Then I expect the text 'Language' to be present
And I expect the text 'Langue' to be absent

View File

@ -0,0 +1,10 @@
Feature: Global 'Theme' setting
Scenario: Change the theme to Mermaid
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
When I tap the "DropdownTheme" button
And I tap the element that contains the text "Mermaid"
Scenario: Change the theme to Light Mode
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
And I tap the widget that contains the text "Theme"

View File

@ -0,0 +1,20 @@
@env:aliceandbob1
Feature: Block unknown contacts setting
Scenario: Carol adds Alice but Alice doesn't see it because Block Unknowns is enabled
Given I wait until the widget with type 'ProfileMgrView' is present
Given I tap the 'OpenSettingsView' button
When I tap the widget that contains the text "Block Unknown Contacts"
Then I expect the switch that contains the text "Block Unknown Contacts" to be checked
Given I tap the back button
And I wait until the text "Carol" is present
And I tap the button that contains the text "Carol"
And I tap the button with tooltip "Add a new contact or conversation"
When I fill the "txtAddP2P" field with "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd"
And I wait for 1 second
And I take a screenshot
And I tap the back button
And I wait until the text "Alice" is present
And I wait until the tooltip "Online" is present
And I tap the button that contains the text "Alice"
And I wait for 20 seconds
Then I expect the text "yxj2pvhozedflp4g7yitpqkeho63maaffi2qgsj3e6s2fbmosuuas2qd" to be absent

View File

@ -0,0 +1,18 @@
@env:aliceandbob1
Feature: Streamer mode
Scenario: All onions disappear when Streamer Mode is enabled
Given I wait until the widget with type 'ProfileMgrView' is present
And I wait until the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" is present
And I wait until the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" is present
Given I tap the 'OpenSettingsView' button
And I wait for 1 second
And I tap the widget that contains the text "Streamer/Presentation Mode"
Then I expect the switch that contains the text "Streamer/Presentation Mode" to be checked
When I tap the back button
And I wait until the text "Alice" is present
And I wait until the text "Bob" is present
Then I expect the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" to be absent
And I expect the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" to be absent
When I tap the button that contains the text "Alice"
Then I expect the text "vbmmsbx3rhndpfz6t3jkrd7m3yu62xzrldxkdgsw4rsehiwuw3tmo7yd" to be absent
And I expect the text "pjurzypqui3dnpxj6aemk6cqz22yx6zfr5lq4jzu7muwe2yyx2zrnzyd" to be absent

View File

@ -0,0 +1,90 @@
@env:persist
Feature: Basic Profile Management
Scenario: Error on Creating a Profile without a Display Name
Given I wait until the widget with type 'ProfileMgrView' is present
And I tap the button with tooltip "Add new profile"
Then I expect the text 'Display Name' to be present
And I expect the text 'New Password' to be present
And I expect the text 'Please enter a display name' to be absent
Then I tap the "button" widget with label "Add new profile"
And I expect the text 'Please enter a display name' to be present
And I take a screenshot
Scenario: Create Unencrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
And I tap the button with tooltip "Add new profile"
Then I expect the text 'Display Name' to be present
And I expect the text 'New Password' to be present
And I take a screenshot
Then I tap the "passwordCheckBox" widget
And I expect the text 'New Password' to be absent
And I take a screenshot
Then I fill the "displayNameFormElement" field with "Alice (Unencrypted)"
Then I tap the "button" widget with label "Add new profile"
And I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
And I take a screenshot
Then I tap the "ProfileRow" widget with label "Alice (Unencrypted)"
And I expect the text "Alice (Unencrypted) » Conversations" to be present
And I take a screenshot
Scenario: Load Unencrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
Scenario: Create Encrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
And I tap the button with tooltip "Add new profile"
Then I expect the text 'Display Name' to be present
And I expect the text 'New Password' to be present
And I take a screenshot
Then I fill the "displayNameFormElement" field with "Alice (Encrypted)"
Then I fill the "passwordFormElement" field with "password1"
Then I fill the "confirmPasswordFormElement" field with "password1"
And I take a screenshot
Then I tap the "button" widget with label "Add new profile"
And I expect a "ProfileRow" widget with text "Alice (Encrypted)"
And I take a screenshot
Then I tap the "ProfileRow" widget with label "Alice (Encrypted)"
And I expect the text 'Alice (Encrypted) » Conversations' to be present
And I take a screenshot
Scenario: Load an Encrypted Profile by Unlocking it with a Password
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
Scenario: Load an Encrypted Profile by Unlocking it with a Password and Change the Name
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
When I tap the "IconButton" widget with tooltip "Edit Profile Alice (Encrypted)"
Then I expect the text 'Display Name' to be present
Then I fill the "displayNameFormElement" field with "Carol (Encrypted)"
And I tap the "button" widget with label "Save Profile"
And I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
Scenario: Delete an Encrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
And I take a screenshot
When I tap the "IconButton" widget with tooltip "Edit Profile Carol (Encrypted)"
Then I expect the text 'Display Name' to be present
When I tap the button that contains the text "Delete"
Then I expect the text "Really Delete Profile" to be present
When I tap the "button" widget with label "Really Delete Profile"
And I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)" to be absent

View File

@ -0,0 +1,31 @@
@env:aliceandbob1
Feature: Sending and receiving chat messages
Background:
Given I wait until the widget with type "ProfileRow" is present
And I wait for 4 seconds
Given I tap the button that contains the text "Alice"
And I tap the button that contains the text "Bob"
And I wait until the text "Contact is offline, messages can't be delivered right now" is absent
#And I wait for 6 seconds
When I fill the "txtCompose" field with "hello! this is a test!"
And I tap the "btnSend" button
Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds
#Then I expect the text "hello! this is a test!" to be present
And I tap the back button
And I tap the back button
Scenario: Bob receives the message from Alice
Given I tap the button that contains the text "Bob"
And I tap the button that contains the text "Alice"
Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds
Scenario: Bob replies to a message from Alice
Given I tap the button that contains the text "Bob"
And I tap the button that contains the text "Alice"
#When I swipe right by 15 pixels on the element that contains the text "hello! this is a test!\u202F"
#When I swipe right by 15 pixels on the widget of type "MessageBubble" with text "hello! this is a test!\u202F"
And I tap the button with tooltip "Reply to this message"
And I fill the "txtCompose" field with "yay the test worked"
And I tap the "btnSend" button
Then I expect to see the message "yay the test worked\u202F" replying to "hello! this is a test!" within 5 seconds
And I take a screenshot

View File

@ -0,0 +1,100 @@
//import 'package:flutter_gherkin/flutter_gherkin_integration_test.dart'; // notice new import name
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gherkin/gherkin.dart';
import 'dart:io';
// The application under test.
import 'package:cwtch/main.dart' as app;
import 'package:glob/glob.dart';
import 'hooks/env.dart';
import 'steps/chat.dart';
import 'steps/files.dart';
import 'steps/form_elements.dart';
import 'steps/overrides.dart';
import 'steps/text.dart';
import 'steps/utils.dart';
part 'gherkin_suite_test.g.dart';
const REPLACED_BY_SCRIPT = <String>['integration_test/features/**.feature'];
@GherkinTestSuite(executionOrder: ExecutionOrder.alphabetical, featurePaths: REPLACED_BY_SCRIPT)
void main() {
final params = [
SwitchStateParameter(),
];
final steps = [
// chat elements
ExpectReply(),
// form elements
CheckSwitchState(),
CheckSwitchStateWithText(),
DropdownChoose(),
// utils
TakeScreenshot(),
// overrides
TapWidgetWithType(),
TapWidgetWithLabel(),
TapWidgetWithTooltip(),
ExpectWidgetWithText(),
AbsentWidgetWithText(),
WaitUntilTypeExists(),
ExpectTextToBePresent(),
ExpectWidgetWithTextWithin(),
WaitUntilTextExists(),
WaitUntilTooltipExists(),
SwipeOnType(),
// text
TorVersionPresent(),
TooltipTap(),
// files
FolderExists(),
FileExists(),
];
var sb = StringBuffer();
sb..writeln("## Custom Parameters\n")
..writeln("| name | pattern |")
..writeln("| --- | --- |");
for (var i in params) {
sb..write("| ")..write(i.identifier)..write(" | ")..write(i.pattern.toString().replaceFirst("RegExp: pattern=","").replaceFirst(" flags=i","").replaceAll("|", "&#124;"))..writeln(" |");
}
sb..writeln("\n## Custom steps\n")
..writeln("| pattern |")
..writeln("| --- |");
for (var i in steps) {
sb.writeln(i.pattern.toString().replaceFirst("RegExp: pattern=", "| ").replaceFirst(" flags=", " |").replaceAll("|", "&#124;"));
}
var f = File("integration_test/CustomSteps.md");
f.writeAsString(sb.toString());
executeTestSuite(
FlutterTestConfiguration.DEFAULT([])
..reporters = [
StdoutReporter(MessageLevel.error)
..setWriteLineFn(print)
..setWriteFn(print),
ProgressReporter()
..setWriteLineFn(print)
..setWriteFn(print),
TestRunSummaryReporter()
..setWriteLineFn(print)
..setWriteFn(print),
JsonReporter(
writeReport: (_, __) => Future<void>.value(),
),
]
..customStepParameterDefinitions = [
SwitchStateParameter(),
]
..stepDefinitions = steps
..hooks = [
ResetCwtchEnvironment(),
AttachScreenshotOnFailedStepHook(),
],
(World world) => app.main(),
);
}

View File

@ -0,0 +1,48 @@
import 'dart:io';
import 'package:gherkin/gherkin.dart';
class ResetCwtchEnvironment extends Hook {
@override
int get priority => 10;
@override
Future<void> onBeforeRun(TestConfiguration config) async {
// initialize @env:persist
await Process.run("rm", ["-rf", "integration_test/env/temp-persist"]);
await Process.run("cp", ["-R", "integration_test/env/persist", "integration_test/env/temp-persist"]);
return super.onBeforeRun(config);
}
@override
Future<void> onAfterRun(TestConfiguration config) async {
await Process.run("rm", ["-rf", "integration_test/env/temp-persist"]);
return super.onAfterRun(config);
}
@override
Future<void> onBeforeScenario(TestConfiguration config, String scenario, Iterable<Tag> tags) async {
if (tags.any((t) => t.name == "@env:persist")) {
await Process.run("mv", ["integration_test/env/temp-persist", "integration_test/env/temp"]);
} else if (tags.any((t) => t.name == "@env:aliceandbob1")) {
await Process.run("cp", ["-R", "integration_test/env/aliceandbob1", "integration_test/env/temp"]);
} else if (!(tags.any((t) => t.name == "@env:clean"))) {
// use the default environment if no @env: tag specified
await Process.run("cp", ["-R", "integration_test/env/default", "integration_test/env/temp"]);
} else {
print("no environment initialized");
}
return super.onBeforeScenario(config, scenario, tags);
}
@override
Future<void> onAfterScenario(TestConfiguration config, String scenario, Iterable<Tag> tags) async {
if (tags.any((t) => t.name == "@env:persist")) {
await Process.run("mv", ["integration_test/env/temp", "integration_test/env/temp-persist"]);
} else {
await Process.run("rm", ["-rf", "integration_test/env/temp"]);
}
return super.onAfterScenario(config, scenario, tags);
}
}

View File

@ -0,0 +1,42 @@
import 'package:cwtch/main.dart';
import 'package:cwtch/widgets/messagebubble.dart';
import 'package:cwtch/widgets/profilerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:cwtch/views/profilemgrview.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart';
import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart';
import 'package:gherkin/gherkin.dart';
import 'package:flutter/material.dart';
import 'overrides.dart';
StepDefinitionGeneric ExpectReply() {
return given3<String, String, int, FlutterWorld>(
RegExp(
r'I expect to see the message {string} replying to {string} within {int} second(s)$'),
(originalMessage, responseMessage, seconds, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return await context.world.appDriver.isPresent(
context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(QuotedMessageBubble, FindType.type),
context.world.appDriver.findBy(originalMessage, FindType.text)
)
) && await context.world.appDriver.isPresent(
context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(QuotedMessageBubble, FindType.type),
context.world.appDriver.findBy(responseMessage, FindType.text)
));
},
timeout: Duration(seconds: seconds),
);
},
configuration: StepDefinitionConfiguration()
..timeout = const Duration(days: 1),
);
}

View File

@ -0,0 +1,23 @@
import 'dart:io';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
StepDefinitionGeneric FolderExists() {
return then1<String, FlutterWorld>(
RegExp(
r'I expect the folder {string} to exist'),
(input1, context) async {
context.expect(Directory(input1).existsSync(), true);
},
);
}
StepDefinitionGeneric FileExists() {
return then1<String, FlutterWorld>(
RegExp(
r'I expect the file {string} to exist'),
(input1, context) async {
context.expect(File(input1).existsSync(), true);
},
);
}

View File

@ -0,0 +1,81 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum SwitchState { checked, unchecked }
class SwitchStateParameter extends CustomParameter<SwitchState> {
SwitchStateParameter()
: super("toggle", RegExp(r"(checked|unchecked)", caseSensitive: false), (s) {
switch (s.toLowerCase()) {
case "checked":
return SwitchState.checked;
case "unchecked":
return SwitchState.unchecked;
}
});
}
class CheckSwitchState extends Given2WithWorld<String,SwitchState,FlutterWorld> {
@override
Future<void> executeStep(String input1, SwitchState state) async {
final switch1 = world.appDriver.findBy(input1, FindType.key);
bool switch1exists = await world.appDriver.isPresent(switch1);
expect(switch1exists, true);
if (switch1exists) {
SwitchListTile wdgt = await world.appDriver.widget(switch1);
expect(wdgt.value, state == SwitchState.checked);
}
}
@override
RegExp get pattern => RegExp(r"I expect the {string} widget to be {toggle}");
}
StepDefinitionGeneric CheckSwitchStateWithText() {
return then2<String, SwitchState, FlutterWorld>(
RegExp(
r'I expect the switch that contains the text {string} to be {toggle}'),
(input1, state, context) async {
final textFinder = context.world.appDriver.findBy(input1, FindType.text);
await context.world.appDriver.scrollIntoView(textFinder);
final switchTypeFinder = context.world.appDriver.findBy(SwitchListTile, FindType.type);
final switchFinder = context.world.appDriver.findByAncestor(textFinder, switchTypeFinder);
SwitchListTile switchWidget = await context.world.appDriver.widget(switchFinder);
context.expect(switchWidget.value, state == SwitchState.checked);
},
);
}
StepDefinitionGeneric DropdownChoose() {
return then2<int, String, FlutterWorld>(
RegExp(
r'I choose option {int} from the {string} dropdown'),
(idx, input1, context) async {
await context.world.appDriver.waitForAppToSettle();
final ddFinder = context.world.appDriver.findBy(input1, FindType.key);
await context.world.appDriver.scrollIntoView(ddFinder);
await context.world.appDriver.waitForAppToSettle();
await context.world.appDriver.tap(ddFinder);
await context.world.appDriver.waitForAppToSettle();
// somewhat complicated due to widget structure... we need to:
// find [ancestor of type DropdownMenuItem] of [[Text with value <text of element #idx>] contained within Dropdown]
DropdownButton ddWidget = await context.world.appDriver.widget(ddFinder);
DropdownMenuItem itemWidget = ddWidget.items!.elementAt(idx);
final itemText = (itemWidget.child as Text).data.toString();
final textFinder = context.world.appDriver.findBy(itemText, FindType.text);
final textWithinFinder = context.world.appDriver.findByDescendant(ddFinder, textFinder);
final ddiFinder = context.world.appDriver.findBy(DropdownMenuItem<String>, FindType.type);
//final ddiFinder = context.world.appDriver.findBy(_MenuItem, FindType.type);
final itemFinder = context.world.appDriver.findByAncestor(textWithinFinder, ddiFinder, firstMatchOnly: true);
await context.world.appDriver.tap(itemFinder);
await context.world.appDriver.waitForAppToSettle();
},
);
}

View File

@ -0,0 +1,295 @@
// this file contains steps from flutter_gherkin with bugfixes/adaptations to our codebase
import 'package:cwtch/main.dart';
import 'package:cwtch/widgets/messagebubble.dart';
import 'package:cwtch/widgets/profilerow.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:cwtch/views/profilemgrview.dart';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:flutter_gherkin/src/flutter/parameters/existence_parameter.dart';
import 'package:flutter_gherkin/src/flutter/parameters/swipe_direction_parameter.dart';
import 'package:gherkin/gherkin.dart';
import 'package:flutter/material.dart';
StepDefinitionGeneric TapWidgetWithType() {
return given1<String, FlutterWorld>(
RegExp(r'I tap the (?:button|element|label|icon|field|text|widget) with type {string}$'),
(input1, context) async {
await context.world.appDriver.tap(
context.world.appDriver.findBy(
widgetTypeByName(input1),
FindType.type,
),
);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric TapWidgetWithLabel() {
return given2<String, String, FlutterWorld>(
RegExp(r'I tap the {string} widget with label {string}$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.text),
firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
//print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.scrollIntoView(finder);
await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric TapWidgetWithTooltip() {
return given2<String, String, FlutterWorld>(
RegExp(r'I tap the {string} widget with tooltip {string}$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.tooltip),
firstMatchOnly: true);
await context.world.appDriver.scrollIntoView(finder);
await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric ExpectWidgetWithText() {
return given2<String, String, FlutterWorld>(
RegExp(r'I expect a {string} widget with text {string}$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.text),
firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
//print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.isPresent(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric AbsentWidgetWithText() {
return given2<String, String, FlutterWorld>(
RegExp(r'I expect a {string} widget with text {string} to be absent$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.text),
firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
//print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.isAbsent(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric TapButtonWithText() {
return given1<String, FlutterWorld>(
RegExp(r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'),
(input1, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(Flwtch, FindType.type),
context.world.appDriver.findBy(input1, FindType.key),
firstMatchOnly: true);
await context.world.appDriver.scrollIntoView(finder);
await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric WaitUntilTypeExists() {
return then2<String, Existence, FlutterWorld>(
'I wait until the (?:button|element|label|icon|field|text|widget) with type {string} is {existence}',
(ofType, existence, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return existence == Existence.absent
? context.world.appDriver.isAbsent(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
)
: context.world.appDriver.isPresent(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
);
},
);
},
);
}
StepDefinitionGeneric ExpectTextToBePresent() {
return given2<String, int, FlutterWorld>(
RegExp(
r'I expect the string {string} to be present within {int} second(s)$'),
(key, seconds, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return context.world.appDriver.isPresent(
context.world.appDriver.findBy(key, FindType.text),
);
},
timeout: Duration(seconds: seconds),
);
},
configuration: StepDefinitionConfiguration()
..timeout = const Duration(days: 1),
);
}
StepDefinitionGeneric ExpectWidgetWithTextWithin() {
return given3<String, String, int, FlutterWorld>(
RegExp(
r'I expect a {string} widget with text {string} to be present within {int} second(s)$'),
(widgetType, text, seconds, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return context.world.appDriver.isPresent(
context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(widgetType), FindType.type),
context.world.appDriver.findBy(text, FindType.text)
),
);
},
timeout: Duration(seconds: seconds),
);
},
configuration: StepDefinitionConfiguration()
..timeout = const Duration(days: 1),
);
}
StepDefinitionGeneric WaitUntilTextExists() {
return then2<String, Existence, FlutterWorld>(
'I wait until the text {string} is {existence}',
(text, existence, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return existence == Existence.absent
? context.world.appDriver.isAbsent(
context.world.appDriver.findBy(text, FindType.text),
)
: context.world.appDriver.isPresent(
context.world.appDriver.findBy(text, FindType.text),
);
},
timeout: Duration(seconds: 120),
);
},
configuration: StepDefinitionConfiguration()
..timeout = const Duration(days: 1),
);
}
StepDefinitionGeneric WaitUntilTooltipExists() {
return then2<String, Existence, FlutterWorld>(
'I wait until the tooltip {string} is {existence}',
(ofType, existence, context) async {
await context.world.appDriver.waitUntil(
() async {
await context.world.appDriver.waitForAppToSettle();
return existence == Existence.absent
? context.world.appDriver.isAbsent(
context.world.appDriver.findBy(ofType, FindType.tooltip),
)
: context.world.appDriver.isPresent(
context.world.appDriver.findBy(ofType, FindType.tooltip),
);
},
timeout: Duration(seconds: 120),
);
},
configuration: StepDefinitionConfiguration()
..timeout = const Duration(days: 1),
);
}
mixin _SwipeHelper
on When4WithWorld<SwipeDirection, int, String, String, FlutterWorld> {
Future<void> swipeOnFinder(
dynamic finder,
SwipeDirection direction,
int swipeAmount,
) async {
if (direction == SwipeDirection.left || direction == SwipeDirection.right) {
final offset =
direction == SwipeDirection.right ? swipeAmount : (swipeAmount * -1);
await world.appDriver.scroll(
finder,
dx: offset.toDouble(),
duration: Duration(milliseconds: 500),
timeout: timeout,
);
} else {
final offset =
direction == SwipeDirection.up ? swipeAmount : (swipeAmount * -1);
await world.appDriver.scroll(
finder,
dy: offset.toDouble(),
duration: Duration(milliseconds: 500),
timeout: timeout,
);
}
}
}
class SwipeOnType
extends When4WithWorld<SwipeDirection, int, String, String, FlutterWorld>
with _SwipeHelper {
@override
Future<void> executeStep(
SwipeDirection direction,
int swipeAmount,
String typeOf,
String text,
) async {
final finder = this.world.appDriver.findByDescendant(
this.world.appDriver.findBy(widgetTypeByName(typeOf), FindType.type),
this.world.appDriver.findBy(text, FindType.text)
);
await swipeOnFinder(finder, direction, swipeAmount);
}
@override
RegExp get pattern => RegExp(
r'I swipe {swipe_direction} by {int} pixels on the widget of type {string} with text {string}');
}
Type widgetTypeByName(String input1) {
switch (input1) {
case "MessageBubble":
return MessageBubble;
case "ProfileMgrView":
return ProfileMgrView;
case "ProfileRow":
return ProfileRow;
case "TorIcon":
return TorIcon;
case "button":
return ElevatedButton;
case "IconButton":
return IconButton;
case "ProfileRow":
return ProfileRow;
default:
throw("Unknown type $input1. add to integration_test/features/overrides.dart");
}
}

View File

@ -0,0 +1,49 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
StepDefinitionGeneric TooltipTap() {
return given1<String, FlutterWorld>(
RegExp(r'I tap the button with tooltip {string}'),
(input1, context) async {
final finder = context.world.appDriver.findBy(input1, FindType.tooltip);
await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle();
}
);
}
StepDefinitionGeneric TorVersionPresent() {
return given<FlutterWorld>(
RegExp(r'I expect the Tor version to be present$'),
(context) async {
String versionString = "";
final file = File('fetch-tor.sh');
Stream<String> lines = file.openRead()
.transform(utf8.decoder)
.transform(LineSplitter());
try {
await for (var line in lines) {
if (line.startsWith("wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-")) {
versionString = line.substring(81, 88);
break;
}
}
print('File is now closed.');
} catch (e) {
print('Error: $e');
}
if (versionString == "") {
context.expect(versionString, "#.#.#", reason: "error reading version string from fetch-tor.sh");
return;
}
context.world.attach(versionString, "text/plain", "Then I expect the Tor version to be present");
context.reporter.message("test!!!", MessageLevel.info);
print("looking for version string $versionString");
final finder = context.world.appDriver.findBy(versionString, FindType.text,);
final isP = await context.world.appDriver.isPresent(finder);
context.expect(isP, true);
},
);
}

View File

@ -0,0 +1,19 @@
import 'dart:convert';
import 'package:flutter_gherkin/flutter_gherkin.dart';
import 'package:gherkin/gherkin.dart';
StepDefinitionGeneric TakeScreenshot() {
return then<FlutterWorld>(
RegExp(
r'I take a screenshot'),
(context) async {
try {
final bytes = await context.world.appDriver.screenshot();
final screenshotData = base64Encode(bytes);
context.world.attach(screenshotData, 'image/png', 'And I take a screenshot');
} catch (e, st) {
context.world.attach('Failed to take screenshot\n$e\n$st', 'text/plain');
}
},
);
}

View File

@ -0,0 +1,5 @@
Feature: Splash screen displays and then closes
Scenario: splash screen appears
Then I expect the widget 'SplashView' to be present within 1 second
Scenario: splash screen completes
Then I expect the widget 'ProfileManagerView' to be present within 10 seconds

View File

@ -0,0 +1,13 @@
Feature: Settings pane opens and can save settings
Scenario: Open the Settings pane
Given I tap the 'OpenSettingsView' button
Then I expect the text 'Cwtch Settings' to be present
Scenario: Change the 'Block unknown contacts' setting and restart Cwtch
When I tap the 'OpenSettingsView' button
Then I wait for 6 seconds
Then I expect the 'SwitchBlockUnknown' widget to be unchecked
Then I tap the 'SwitchBlockUnknown' widget
Then I expect the 'SwitchBlockUnknown' widget to be checked
Then I tap the back button
Then I tap the 'OpenSettingsView' button
Then I expect the 'SwitchBlockUnknown' widget to be checked

View File

@ -3,6 +3,10 @@ const dev_version = "development";
class EnvironmentConfig {
static const BUILD_VER = String.fromEnvironment('BUILD_VER', defaultValue: dev_version);
static const BUILD_DATE = String.fromEnvironment('BUILD_DATE', defaultValue: "now");
// set by the automated testing harness to circumvent untestable behaviours
// for example:
// * MessageRow: always show "reply" button (because can't test onHover or swipe)
static const TEST_MODE = String.fromEnvironment('TEST_MODE', defaultValue: "false") == "true";
static void debugLog(String log) {
if (EnvironmentConfig.BUILD_VER == dev_version) {

3
lib/constants.dart Normal file
View File

@ -0,0 +1,3 @@
const int MaxImageFileSharingSize = 20971520;
const int MaxGeneralFileSharingSize = 10737418240;

View File

@ -0,0 +1,43 @@
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void showPasswordDialog(BuildContext context, String title, String action, Function(String) onEntered) {
TextEditingController passwordController = TextEditingController();
CwtchPasswordField passwordField = CwtchPasswordField(
controller: passwordController,
validator: (passsword) {
return null;
});
// set up the buttons
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = ElevatedButton(
child: Text(action),
onPressed: () {
onEntered(passwordController.value.text);
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(title),
content: passwordField,
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

View File

@ -0,0 +1,58 @@
import 'dart:io';
import 'package:cwtch/models/appstate.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Function onError, Function onCancel) async {
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
FilePickerResult? result = await FilePicker.platform.pickFiles(lockParentWindow: true);
appstate.disableFilePicker = false;
if (result != null && result.files.first.path != null) {
File file = File(result.files.first.path!);
// We have a maximum number of bytes we can represent in terms of
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
if (file.lengthSync() <= maxBytes) {
onSuccess(file);
} else {
onError();
}
} else {
onCancel();
}
}
Future<String?> showCreateFilePicker(BuildContext ctx) async {
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
String? result = await FilePicker.platform.saveFile(lockParentWindow: true);
appstate.disableFilePicker = false;
return result;
}
Future<String?> showSelectDirectoryPicker(BuildContext ctx) async {
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
String? result = await FilePicker.platform.getDirectoryPath(lockParentWindow: true);
appstate.disableFilePicker = false;
return result;
}

View File

@ -0,0 +1,61 @@
import 'package:cwtch/third_party/linkify/linkify.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(bcontext)!.clickableLinksWarning),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinksCopy, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinksCopy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.copiedToClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinkOpen, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinkOpen),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.clickableLinkError),
);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
}
},
),
),
]),
],
)),
));
});
}

View File

@ -1,9 +1,9 @@
import 'package:flutter/src/services/text_input.dart';
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefaultPassword = "be gay do crime";
const LastMessageSeenTimeKey = "profile.lastMessageSeenTime";
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
@ -19,6 +19,11 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
void ChangePassword(String profile, String pass, String newpass, String newpassAgain);
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file);
// ignore: non_constant_identifier_names
Future<dynamic> ImportProfile(String file, String pass);
// ignore: non_constant_identifier_names
void ResetTor();
@ -45,12 +50,15 @@ abstract class Cwtch {
Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
// ignore: non_constant_identifier_names
void SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, int handle, int target);
Future<dynamic> GetMessages(String profile, int handle, int index, int count);
// ignore: non_constant_identifier_names
void ShareFile(String profile, int handle, String filepath);
Future<dynamic> SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
Future<dynamic> SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names
Future<dynamic> ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// android-only
@ -100,10 +108,16 @@ abstract class Cwtch {
void SetServerAttribute(String serverOnion, String key, String val);
// ignore: non_constant_identifier_names
void Shutdown();
Future<void> Shutdown();
// non-ffi
String defaultDownloadPath();
bool isL10nInit();
void l10nInit(String notificationSimple, String notificationConversationInfo);
void dispose();
Future<dynamic> GetDebugInfo();
}

View File

@ -9,6 +9,7 @@ import 'package:cwtch/models/remoteserver.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:cwtch/torstatus.dart';
@ -17,6 +18,8 @@ import '../config.dart';
import '../errorHandler.dart';
import '../settings.dart';
typedef SeenMessageCallback = Function(String, int, DateTime);
// Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
// Takes Notifiers and triggers them on appropriate events
class CwtchNotifier {
@ -28,6 +31,11 @@ class CwtchNotifier {
late AppState appState;
late ServerListState serverListState;
String? notificationSimple;
String? notificationConversationInfo;
SeenMessageCallback? seenMessageCallback;
CwtchNotifier(
ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
profileCN = pcn;
@ -39,6 +47,15 @@ class CwtchNotifier {
serverListState = serverListStateCN;
}
void l10nInit(String notificationSimple, String notificationConversationInfo) {
this.notificationSimple = notificationSimple;
this.notificationConversationInfo = notificationConversationInfo;
}
void setMessageSeenCallback(SeenMessageCallback callback) {
seenMessageCallback = callback;
}
void handleMessage(String type, dynamic data) {
//EnvironmentConfig.debugLog("NewEvent $type $data");
switch (type) {
@ -49,30 +66,33 @@ class CwtchNotifier {
appState.SetAppError(data["Error"]);
break;
case "NewPeer":
// empty events can be caused by the testing framework
if (data["Online"] == null) {
break;
}
EnvironmentConfig.debugLog("NewPeer $data");
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
profileCN.add(data["Identity"], data["name"], data["picture"], data["defaultPicture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break;
case "ContactCreated":
EnvironmentConfig.debugLog("ContactCreated $data");
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"],
int.parse(data["ConversationID"]),
data["RemotePeer"],
nickname: data["nick"],
status: data["status"],
imagePath: data["picture"],
blocked: data["blocked"] == "true",
accepted: data["accepted"] == "true",
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]),
isGroup: data["isGroup"] == true,
server: data["groupServer"],
archived: data["isArchived"] == true,
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
));
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["RemotePeer"],
nickname: data["nick"],
status: data["status"],
imagePath: data["picture"],
defaultImagePath: data["defaultPicture"],
blocked: data["blocked"] == "true",
accepted: data["accepted"] == "true",
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]),
isGroup: false, // by definition
server: null,
archived: false,
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
notificationPolicy: data["notificationPolicy"] ?? "ConversationNotificationPolicy.Default"));
break;
case "NewServer":
EnvironmentConfig.debugLog("NewServer $data");
@ -102,12 +122,15 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"],
blocked: false, // we created
accepted: true, // we created
imagePath: data["PicturePath"],
imagePath: data["picture"],
defaultImagePath: data["picture"],
nickname: data["GroupName"],
status: status,
server: data["GroupServer"],
isGroup: true,
lastMessageTime: DateTime.now()));
lastMessageTime: DateTime.now(),
notificationPolicy: data["notificationPolicy"] ?? "ConversationNotificationPolicy.Default"));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(int.parse(data["ConversationID"]), DateTime.now());
}
break;
@ -138,16 +161,27 @@ class CwtchNotifier {
}
break;
case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!");
var identifier = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var senderImage = data['picture'];
var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash'];
String contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
var notification = data["notification"];
if (selectedConversation && seenMessageCallback != null) {
seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
}
if (notification == "SimpleEvent") {
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
} else if (notification == "ContactInfo") {
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
}
profileCN.getProfile(data["ProfileOnion"])?.newMessage(
identifier,
@ -170,39 +204,29 @@ class CwtchNotifier {
var conversation = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation);
// We return -1 for protocol message acks if there is no message
if (messageID == -1) break;
var key = contact!.getMessageKeyOrFail(conversation, messageID);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
message.ackd = true;
// We only ever see acks from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
} catch (e) {
// ignore, most likely cause is the key got optimized out...
// We only ever see acks from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
break;
case "NewMessageFromGroup":
var identifier = int.parse(data["ConversationID"]);
if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]);
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var senderImage = data['picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var currentTotal = contact!.totalMessages;
var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash'];
String contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
var notification = data["notification"];
// Only bother to do anything if we know about the group and the provided index is greater than our current total...
if (currentTotal != null && idx >= currentTotal) {
@ -217,11 +241,19 @@ class CwtchNotifier {
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
// and `local now`.
profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation);
if (selectedConversation && seenMessageCallback != null) {
seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
}
notificationManager.notify("New Message From Group!");
if (notification == "SimpleEvent") {
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
} else if (notification == "ContactInfo") {
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
}
appState.notifyProfileUnread();
}
RemoteServerInfoState? server = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(contact.server);
RemoteServerInfoState? server = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(contact.server ?? "");
server?.updateSyncProgressFor(timestampSent);
} else {
// This is dealt with by IndexedAcknowledgment
@ -231,12 +263,8 @@ class CwtchNotifier {
case "IndexedFailure":
var identifier = int.parse(data["ConversationID"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var idx = int.parse(data["Index"]);
var key = contact?.getMessageKeyOrFail(contact.identifier, idx);
if (key != null) {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
message.error = true;
}
var messageID = int.parse(data["Index"]);
contact!.errCache(messageID);
break;
case "AppError":
EnvironmentConfig.debugLog("New App Error: $data");
@ -253,12 +281,14 @@ class CwtchNotifier {
case "UpdatedProfileAttribute":
if (data["Key"] == "public.profile.name") {
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else if (data["Key"].toString().startsWith("local.filesharing.") && data["Key"].toString().endsWith(".path")) {
// local.conversation.filekey.path
List<String> keyparts = data["Key"].toString().split(".");
if (keyparts.length == 5) {
String filekey = keyparts[2] + "." + keyparts[3];
profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathForSender(filekey, data["Data"]);
} else if (data["Key"].toString().startsWith("local.filesharing.")) {
if (data["Key"].toString().endsWith(".path")) {
// local.conversation.filekey.path
List<String> keyparts = data["Key"].toString().split(".");
if (keyparts.length == 5) {
String filekey = keyparts[2] + "." + keyparts[3];
profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathForSender(filekey, data["Data"]);
}
}
} else {
EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}");
@ -297,12 +327,12 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
blocked: false, // NewGroup only issued on accepting invite
accepted: true, // NewGroup only issued on accepting invite
imagePath: data["PicturePath"],
imagePath: data["picture"],
nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"],
status: status,
isGroup: true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0)));
lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.fromMillisecondsSinceEpoch(0));
}
}
@ -318,15 +348,22 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
break;
case "NewRetValMessageFromPeer":
if (data["Path"] == "profile.name") {
if (data["Path"] == "profile.name" && data["Exists"] == "true") {
if (data["Data"].toString().trim().length > 0) {
// Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
}
}
} else if (data['Path'] == "profile.picture") {
// Not yet..
} else if (data['Path'] == "profile.custom-profile-image") {
if (data["Exists"] == "true") {
EnvironmentConfig.debugLog("received ret val of custom profile image: $data");
String fileKey = data['Data'];
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
if (contact != null) {
profileCN.getProfile(data["ProfileOnion"])?.waitForDownloadComplete(contact.identifier, fileKey);
}
}
} else {
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
}

View File

@ -52,12 +52,18 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef string_string_to_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringFromStringStringFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
typedef get_json_blob_from_string_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
@ -65,6 +71,9 @@ typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
typedef get_json_blob_from_str_int_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int, int);
typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
Pointer<Utf8>,
@ -93,6 +102,9 @@ typedef VoidFromStringIntIntFn = void Function(Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function();
typedef void_to_string = Pointer<Utf8> Function();
typedef StringFromVoid = Pointer<Utf8> Function();
const String UNSUPPORTED_OS = "unsupported-os";
class CwtchFfi implements Cwtch {
@ -100,6 +112,7 @@ class CwtchFfi implements Cwtch {
late CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate;
ReceivePort _receivePort = ReceivePort();
bool _isL10nInit = false;
static String getLibraryPath() {
if (Platform.isWindows) {
@ -123,6 +136,7 @@ class CwtchFfi implements Cwtch {
}
library = DynamicLibrary.open(libraryPath);
cwtchNotifier = _cwtchNotifier;
cwtchNotifier.setMessageSeenCallback((String profile, int conversation, DateTime time) => {this.SetConversationAttribute(profile, conversation, LastMessageSeenTimeKey, time.toIso8601String())});
}
// ignore: non_constant_identifier_names
@ -146,7 +160,13 @@ class CwtchFfi implements Cwtch {
}
} else if (Platform.isWindows) {
cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
bundledTor = "Tor\\Tor\\tor.exe";
String currentTor = path.join(Directory.current.absolute.path, "Tor\\Tor\\tor.exe");
if (await File(currentTor).exists()) {
bundledTor = currentTor;
} else {
String exeDir = path.dirname(Platform.resolvedExecutable);
bundledTor = path.join(exeDir, "Tor\\Tor\\tor.exe");
}
} else if (Platform.isMacOS) {
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
if (await File("Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
@ -195,8 +215,9 @@ class CwtchFfi implements Cwtch {
// ignore: non_constant_identifier_names
final StartCwtch = startCwtchC.asFunction<StartCwtchFn>();
final ut8CwtchDir = cwtchDir.toNativeUtf8();
StartCwtch(ut8CwtchDir, ut8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
final utf8CwtchDir = cwtchDir.toNativeUtf8();
StartCwtch(utf8CwtchDir, utf8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
malloc.free(utf8CwtchDir);
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
@ -296,6 +317,20 @@ class CwtchFfi implements Cwtch {
return jsonMessage;
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, int handle, int index, int count) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_int_function>>("c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrIntIntIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessages(utf8profile, utf8profile.length, handle, index, count);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@override
// ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String json) {
@ -355,39 +390,48 @@ class CwtchFfi implements Cwtch {
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, int contactHandle, String message) {
var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
Future<dynamic> SendMessage(String profileOnion, int contactHandle, String message) async {
var sendMessage = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_SendMessage");
// ignore: non_constant_identifier_names
final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
final SendMessage = sendMessage.asFunction<GetJsonBlobFromStrIntStrFn>();
final u1 = profileOnion.toNativeUtf8();
final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, contactHandle, u3, u3.length);
Pointer<Utf8> jsonMessageBytes = SendMessage(u1, u1.length, contactHandle, u3, u3.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1);
malloc.free(u3);
return jsonMessage;
}
@override
// ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, int contactHandle, int target) {
var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
Future<dynamic> SendInvitation(String profileOnion, int contactHandle, int target) async {
var sendInvitation = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_SendInvitation");
// ignore: non_constant_identifier_names
final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
final SendInvitation = sendInvitation.asFunction<GetJsonBlobFromStrIntIntFn>();
final u1 = profileOnion.toNativeUtf8();
SendInvitation(u1, u1.length, contactHandle, target);
Pointer<Utf8> jsonMessageBytes = SendInvitation(u1, u1.length, contactHandle, target);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1);
return jsonMessage;
}
@override
// ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int contactHandle, String filepath) {
var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
Future<dynamic> ShareFile(String profileOnion, int contactHandle, String filepath) async {
var shareFile = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
final ShareFile = shareFile.asFunction<GetJsonBlobFromStrIntStrFn>();
final u1 = profileOnion.toNativeUtf8();
final u3 = filepath.toNativeUtf8();
ShareFile(u1, u1.length, contactHandle, u3, u3.length);
Pointer<Utf8> jsonMessageBytes = ShareFile(u1, u1.length, contactHandle, u3, u3.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1);
malloc.free(u3);
return jsonMessage;
}
@override
@ -744,4 +788,54 @@ class CwtchFfi implements Cwtch {
malloc.free(utf8newpass);
malloc.free(utf8newpasssagain);
}
@override
bool isL10nInit() {
return _isL10nInit;
}
@override
void l10nInit(String notificationSimple, String notificationConversationInfo) {
cwtchNotifier.l10nInit(notificationSimple, notificationConversationInfo);
_isL10nInit = true;
}
@override
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file) {
final utf8profile = profile.toNativeUtf8();
final utf8file = file.toNativeUtf8();
var exportProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_ExportProfile");
// ignore: non_constant_identifier_names
final ExportProfileFn = exportProfileC.asFunction<VoidFromStringStringFn>();
ExportProfileFn(utf8profile, utf8profile.length, utf8file, utf8file.length);
malloc.free(utf8profile);
malloc.free(utf8file);
}
@override
// ignore: non_constant_identifier_names
Future<String> ImportProfile(String file, String pass) async {
final utf8pass = pass.toNativeUtf8();
final utf8file = file.toNativeUtf8();
var exportProfileC = library.lookup<NativeFunction<string_string_to_string_function>>("c_ImportProfile");
// ignore: non_constant_identifier_names
final ExportProfileFn = exportProfileC.asFunction<StringFromStringStringFn>();
Pointer<Utf8> result = ExportProfileFn(utf8file, utf8file.length, utf8pass, utf8pass.length);
String importResult = result.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
malloc.free(utf8pass);
malloc.free(utf8file);
return importResult;
}
@override
Future<String> GetDebugInfo() async {
var getDebugInfo = library.lookup<NativeFunction<void_to_string>>("c_GetDebugInfo");
final GetDebugInfo = getDebugInfo.asFunction<StringFromVoid>();
Pointer<Utf8> result = GetDebugInfo();
String debugResult = result.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
return debugResult;
}
}

View File

@ -30,10 +30,12 @@ class CwtchGomobile implements Cwtch {
late Future<dynamic> androidHomeDirectory;
String androidHomeDirectoryStr = "";
late CwtchNotifier cwtchNotifier;
bool _isL10nInit = false;
CwtchGomobile(CwtchNotifier _cwtchNotifier) {
print("gomobile.dart: CwtchGomobile()");
cwtchNotifier = _cwtchNotifier;
cwtchNotifier.setMessageSeenCallback((String profile, int conversation, DateTime time) => {this.SetConversationAttribute(profile, conversation, LastMessageSeenTimeKey, time.toIso8601String())});
androidHomeDirectory = getApplicationDocumentsDirectory();
androidLibraryDir = appInfoPlatform.invokeMethod('getNativeLibDir');
@ -93,6 +95,11 @@ class CwtchGomobile implements Cwtch {
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, int conversation, int index, int count) {
return cwtchPlatform.invokeMethod("GetMessages", {"ProfileOnion": profile, "conversation": conversation, "index": index, "count": count});
}
@override
// ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String jsonEvent) {
@ -128,20 +135,20 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, int conversation, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
Future<dynamic> SendMessage(String profileOnion, int conversation, String message) {
return cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
}
@override
// ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, int conversation, int target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
Future<dynamic> SendInvitation(String profileOnion, int conversation, int target) {
return cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
}
@override
// ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int conversation, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
Future<dynamic> ShareFile(String profileOnion, int conversation, String filepath) {
return cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
}
@override
@ -195,7 +202,7 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void DeleteContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "conversation": conversation});
cwtchPlatform.invokeMethod("DeleteConversation", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
@ -295,4 +302,35 @@ class CwtchGomobile implements Cwtch {
void ChangePassword(String profile, String pass, String newpass, String newpassAgain) {
cwtchPlatform.invokeMethod("ChangePassword", {"ProfileOnion": profile, "OldPass": pass, "NewPass": newpass, "NewPassAgain": newpassAgain});
}
@override
bool isL10nInit() {
return _isL10nInit;
}
@override
void l10nInit(String notificationSimple, String notificationConversationInfo) {
cwtchNotifier.l10nInit(notificationSimple, notificationConversationInfo);
cwtchPlatform.invokeMethod("L10nInit", {"notificationSimple": notificationSimple, "notificationConversationInfo": notificationConversationInfo});
_isL10nInit = true;
}
@override
// ignore: non_constant_identifier_names
void ExportProfile(String profile, String file) {
cwtchPlatform.invokeMethod("ExportProfile", {"ProfileOnion": profile, "file": file});
}
@override
// ignore: non_constant_identifier_names
Future<dynamic> ImportProfile(String file, String pass) {
return cwtchPlatform.invokeMethod("ImportProfile", {"file": file, "pass": pass});
}
@override
Future GetDebugInfo() {
// FIXME: getDebugInfo is less useful for Android so for now
// we don't implement it
return Future.value("{}");
}
}

View File

@ -0,0 +1,445 @@
import 'package:flutter/material.dart';
// Flutter doesn't supported Luxembourgish, or Welsh, so we have to provide our
// own delegate for built-in widget translations...
class MaterialLocalizationDelegate extends LocalizationsDelegate<MaterialLocalizations> {
@override
bool isSupported(Locale locale) {
return locale.languageCode == "lb" || locale.languageCode == "cy";
}
@override
Future<MaterialLocalizations> load(Locale locale) async {
switch (locale.languageCode) {
case "cy":
return MaterialLocalizationCy();
case "lb":
return MaterialLocalizationLu();
}
throw UnimplementedError("unknown language");
}
@override
bool shouldReload(covariant LocalizationsDelegate<MaterialLocalizations> old) {
return false;
}
}
// Support Welsh, Default to English
class MaterialLocalizationCy extends DefaultMaterialLocalizations {}
// Support Luxembourgish, Default to German
class MaterialLocalizationLu extends MaterialLocalizations {
@override
String get aboutListTileTitleRaw => r'Über $applicationName';
@override
String get alertDialogLabel => 'Benachrichtigung';
@override
String get anteMeridiemAbbreviation => 'AM';
@override
String get backButtonTooltip => 'Zurück';
@override
String get calendarModeButtonLabel => 'Zum Kalender wechseln';
@override
String get cancelButtonLabel => 'ABBRECHEN';
@override
String get closeButtonLabel => 'SCHLIEẞEN';
@override
String get closeButtonTooltip => 'Schließen';
@override
String get collapsedIconTapHint => 'Maximieren';
@override
String get continueButtonLabel => 'WEITER';
@override
String get copyButtonLabel => 'Kopieren';
@override
String get cutButtonLabel => 'Ausschneiden';
@override
String get dateHelpText => 'tt.mm.jjjj';
@override
String get dateInputLabel => 'Datum eingeben';
@override
String get dateOutOfRangeLabel => 'Außerhalb des Zeitraums.';
@override
String get datePickerHelpText => 'DATUM AUSWÄHLEN';
@override
String get dateRangeEndDateSemanticLabelRaw => r'Enddatum $fullDate';
@override
String get dateRangeEndLabel => 'Enddatum';
@override
String get dateRangePickerHelpText => 'ZEITRAUM AUSWÄHLEN';
@override
String get dateRangeStartDateSemanticLabelRaw => r'Startdatum $fullDate';
@override
String get dateRangeStartLabel => 'Startdatum';
@override
String get dateSeparator => '.';
@override
String get deleteButtonTooltip => 'Löschen';
@override
String get dialModeButtonLabel => 'Zur Uhrzeitauswahl wechseln';
@override
String get dialogLabel => 'Dialogfeld';
@override
String get drawerLabel => 'Navigationsmenü';
@override
String get expandedIconTapHint => 'Minimieren';
@override
String get firstPageTooltip => 'Erste Seite';
@override
String get hideAccountsLabel => 'Konten ausblenden';
@override
String get inputDateModeButtonLabel => 'Zur Texteingabe wechseln';
@override
String get inputTimeModeButtonLabel => 'Zum Texteingabemodus wechseln';
@override
String get invalidDateFormatLabel => 'Ungültiges Format.';
@override
String get invalidDateRangeLabel => 'Ungültiger Zeitraum.';
@override
String get invalidTimeLabel => 'Geben Sie eine gültige Uhrzeit ein';
@override
String get lastPageTooltip => 'Letzte Seite';
@override
String? get licensesPackageDetailTextFew => null;
@override
String? get licensesPackageDetailTextMany => null;
@override
String? get licensesPackageDetailTextOne => '1 Lizenz';
@override
String get licensesPackageDetailTextOther => r'$licenseCount Lizenzen';
@override
String? get licensesPackageDetailTextTwo => null;
@override
String? get licensesPackageDetailTextZero => 'No licenses';
@override
String get licensesPageTitle => 'Lizenzen';
@override
String get modalBarrierDismissLabel => 'Schließen';
@override
String get moreButtonTooltip => 'Mehr';
@override
String get nextMonthTooltip => 'Nächster Monat';
@override
String get nextPageTooltip => 'Nächste Seite';
@override
String get okButtonLabel => 'OK';
@override
String get openAppDrawerTooltip => 'Navigationsmenü öffnen';
@override
String get pageRowsInfoTitleRaw => r'$firstRow$lastRow von $rowCount';
@override
String get pageRowsInfoTitleApproximateRaw => r'$firstRow$lastRow von etwa $rowCount';
@override
String get pasteButtonLabel => 'Einsetzen';
@override
String get popupMenuLabel => 'Pop-up-Menü';
@override
String get postMeridiemAbbreviation => 'PM';
@override
String get previousMonthTooltip => 'Vorheriger Monat';
@override
String get previousPageTooltip => 'Vorherige Seite';
@override
String get refreshIndicatorSemanticLabel => 'Aktualisieren';
@override
String? get remainingTextFieldCharacterCountFew => null;
@override
String? get remainingTextFieldCharacterCountMany => null;
@override
String? get remainingTextFieldCharacterCountOne => 'Noch 1 Zeichen';
@override
String get remainingTextFieldCharacterCountOther => r'Noch $remainingCount Zeichen';
@override
String? get remainingTextFieldCharacterCountTwo => null;
@override
String? get remainingTextFieldCharacterCountZero => 'TBD';
@override
String get reorderItemDown => 'Nach unten verschieben';
@override
String get reorderItemLeft => 'Nach links verschieben';
@override
String get reorderItemRight => 'Nach rechts verschieben';
@override
String get reorderItemToEnd => 'An das Ende verschieben';
@override
String get reorderItemToStart => 'An den Anfang verschieben';
@override
String get reorderItemUp => 'Nach oben verschieben';
@override
String get rowsPerPageTitle => 'Zeilen pro Seite:';
@override
String get saveButtonLabel => 'SPEICHERN';
@override
ScriptCategory get scriptCategory => ScriptCategory.englishLike;
@override
String get searchFieldLabel => 'Suchen';
@override
String get selectAllButtonLabel => 'Alle auswählen';
@override
String get selectYearSemanticsLabel => 'Jahr auswählen';
@override
String? get selectedRowCountTitleFew => null;
@override
String? get selectedRowCountTitleMany => null;
@override
String? get selectedRowCountTitleOne => '1 Element ausgewählt';
@override
String get selectedRowCountTitleOther => r'$selectedRowCount Elemente ausgewählt';
@override
String? get selectedRowCountTitleTwo => null;
@override
String? get selectedRowCountTitleZero => 'Keine Objekte ausgewählt';
@override
String get showAccountsLabel => 'Konten anzeigen';
@override
String get showMenuTooltip => 'Menü anzeigen';
@override
String get signedInLabel => 'Angemeldet';
@override
String get tabLabelRaw => r'Tab $tabIndex von $tabCount';
@override
TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.HH_colon_mm;
@override
String get timePickerDialHelpText => 'UHRZEIT AUSWÄHLEN';
@override
String get timePickerHourLabel => 'Stunde';
@override
String get timePickerHourModeAnnouncement => 'Stunden auswählen';
@override
String get timePickerInputHelpText => 'ZEIT EINGEBEN';
@override
String get timePickerMinuteLabel => 'Minute';
@override
String get timePickerMinuteModeAnnouncement => 'Minuten auswählen';
@override
String get unspecifiedDate => 'Datum';
@override
String get unspecifiedDateRange => 'Zeitraum';
@override
String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN';
@override
String aboutListTileTitle(String applicationName) {
return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName);
}
@override
String dateRangeEndDateSemanticLabel(String formattedDate) {
// TODO: implement dateRangeEndDateSemanticLabel
throw UnimplementedError();
}
@override
String dateRangeStartDateSemanticLabel(String formattedDate) {
// TODO: implement dateRangeStartDateSemanticLabel
throw UnimplementedError();
}
@override
// TODO: implement firstDayOfWeekIndex
int get firstDayOfWeekIndex => throw UnimplementedError();
@override
String formatCompactDate(DateTime date) {
// TODO: implement formatCompactDate
throw UnimplementedError();
}
@override
String formatDecimal(int number) {
// TODO: implement formatDecimal
throw UnimplementedError();
}
@override
String formatFullDate(DateTime date) {
// TODO: implement formatFullDate
throw UnimplementedError();
}
@override
String formatHour(TimeOfDay timeOfDay, {bool alwaysUse24HourFormat = false}) {
// TODO: implement formatHour
throw UnimplementedError();
}
@override
String formatMediumDate(DateTime date) {
// TODO: implement formatMediumDate
throw UnimplementedError();
}
@override
String formatMinute(TimeOfDay timeOfDay) {
// TODO: implement formatMinute
throw UnimplementedError();
}
@override
String formatMonthYear(DateTime date) {
// TODO: implement formatMonthYear
throw UnimplementedError();
}
@override
String formatShortDate(DateTime date) {
// TODO: implement formatShortDate
throw UnimplementedError();
}
@override
String formatShortMonthDay(DateTime date) {
// TODO: implement formatShortMonthDay
throw UnimplementedError();
}
@override
String formatTimeOfDay(TimeOfDay timeOfDay, {bool alwaysUse24HourFormat = false}) {
// TODO: implement formatTimeOfDay
throw UnimplementedError();
}
@override
String formatYear(DateTime date) {
// TODO: implement formatYear
throw UnimplementedError();
}
@override
String licensesPackageDetailText(int licenseCount) {
// TODO: implement licensesPackageDetailText
throw UnimplementedError();
}
@override
// TODO: implement narrowWeekdays
List<String> get narrowWeekdays => throw UnimplementedError();
@override
String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
// TODO: implement pageRowsInfoTitle
throw UnimplementedError();
}
@override
DateTime? parseCompactDate(String? inputString) {
// TODO: implement parseCompactDate
throw UnimplementedError();
}
@override
String remainingTextFieldCharacterCount(int remaining) {
return remaining.toString();
}
@override
String selectedRowCountTitle(int selectedRowCount) {
return selectedRowCount.toString();
}
@override
String tabLabel({required int tabIndex, required int tabCount}) {
// TODO: implement tabLabel
throw UnimplementedError();
}
@override
TimeOfDayFormat timeOfDayFormat({bool alwaysUse24HourFormat = false}) {
// TODO: implement timeOfDayFormat
throw UnimplementedError();
}
}

Some files were not shown because too many files have changed in this diff Show More