Compare commits

...

403 Commits

Author SHA1 Message Date
9e451460da change to my repo 2020-01-14 19:24:32 +00:00
ffa52c129a Merge pull request #33 from LoveIsGrief/32-docker-image
Docker image
2020-01-14 19:21:35 +00:00
b779fb75a0 docker: Remove incompletes warning from README
#32 - Docker image
2020-01-14 20:11:34 +01:00
fbe6b53278 docker: Make sure build directories are ignored
#32 - Docker image
2020-01-14 19:20:11 +01:00
b2bd95788d docker: Try minimizing size using add-pkg and del-pkg
As described in https://github.com/jlesage/docker-baseimage-gui#addingremoving-packages

#32 - Docker image
2020-01-14 19:19:47 +01:00
83d4a2624b docker: Add bisentenialwrug/muwire to README
To be replaced later by @zlatinb's repo

#32 - Docker image
2020-01-14 18:47:28 +01:00
03e20e21aa Remove unnecessary quotes from properties files
There doesn't seem to be a special treatment of them
 in properties files

#32 - Docker image
2020-01-14 18:42:51 +01:00
8a08955675 Remove quotes from i2cp.tcp.port setting
For some reason it really doesn't like that and
 subsequently can't connect to the host

#32 - Docker image
2020-01-14 17:52:52 +01:00
4ec54ebe54 docker: Quote the IP-address in i2p.properties
#32 - Docker image
2020-01-14 17:36:45 +01:00
758af6f48e docker: Make sure APP_HOME is editable by the user
Otherwise MuWire won't be able to write into the home

#32 - Docker image
2020-01-14 17:14:41 +01:00
a7bdd47fcd docker: Add more files to ignore
Helps with build speed on the local machine

#32 - Docker image
2020-01-14 17:00:07 +01:00
f7caa77a18 docker: Include the MuWire icon for the webview
#32 - Docker image
2020-01-14 16:59:39 +01:00
7641f64536 docker: Add default MuWire.properties without nickname
#32 - Docker image
2020-01-14 16:59:13 +01:00
02baaace48 Merge branch 'master' of https://github.com/zlatinb/muwire into 32-docker-image 2020-01-14 16:48:12 +01:00
d90067ff39 prompt for nickname even if MuWire.properties exists so that docker can ship a MuWire.properties #32 2020-01-14 14:17:18 +00:00
c910a215f5 Add the /incompletes docker volume
It won't be used by default though

#32 - Docker image
2020-01-14 13:07:37 +01:00
65e073b1b9 Use defaults for the i2p.properties
This will help writing custom properties
 as not everthing will have to be specified in them

#32 - Docker image
2020-01-14 12:29:05 +01:00
489a7518c3 Attempt to reduce size a bit more
- Ignore the cruft when building
 - Remove the correct temporary directory

#32 - Docker image
2020-01-14 01:09:39 +01:00
3733e48bbd Force set the port
The default isn't used in the code.
That should be fixed, but I'm too tired right now

#32 - Docker image
2020-01-14 00:29:33 +01:00
c3723a1348 Try to minimize image size
#32 - Docker image
2020-01-14 00:15:01 +01:00
0e0f52bc77 Retry: Set a home directory for the "app" user
Apparently it's done differently in the parent image,
 so we just overwrite it.

Hopefully now the app user will have a home

#32 - Docker image
2020-01-13 23:38:04 +01:00
60b9e990cf Set a home directory for the "app" user
#32 - Docker image
2020-01-13 21:34:50 +01:00
28ad0ae30f Add --name to docker run command
#32 - Docker image
2020-01-13 20:29:28 +01:00
9142de85cd Correct the link to the i2cp_config.png
#32 - Docker image
2020-01-13 19:51:20 +01:00
4eb31c11e3 Write README and cleanup inconsistencies
#32 - Docker image
2020-01-13 18:42:30 +01:00
e8afe358a5 First Dockerfile with GUI that starts
It doesn't continue yet as it seems to be waiting for a connection
 to I2P... or something else 🤷#32 - Docker image
2020-01-13 17:07:56 +01:00
3db4317fc1 more items 2020-01-01 11:26:59 +00:00
5ad2b28527 more items 2020-01-01 09:19:46 +00:00
3036765f81 translations 2019-12-27 12:33:22 +01:00
8f9b1e5a8b supress exceptions if client is stopped 2019-12-24 17:05:36 +00:00
e6d59a2438 stop host persister on shutdown 2019-12-24 05:53:02 +00:00
32609b4779 get rid of dependency on groovy-all 2019-12-23 21:16:24 +00:00
74ac4cfecf remove size filter which was left over from grails experiments 2019-12-23 20:54:16 +00:00
69173c4156 update TODO 2019-12-23 20:09:07 +00:00
6283287bee prevent empty input from sharing the I2P working dir 2019-12-22 22:17:57 +00:00
8e3f76f68c move plugin build instructions to the wiki 2019-12-22 16:51:40 +00:00
574294fdc6 update readme 2019-12-22 16:14:42 +00:00
8bd41546cd proper uploader equality check 2019-12-21 23:15:39 +00:00
ba5425c958 extra check for stopped cache client 2019-12-21 15:56:09 +00:00
22580f002c separate update url from main plugin url 2019-12-21 13:46:42 +00:00
5c773cec80 more css changes from zzz 2019-12-20 16:19:00 +00:00
7df00e6709 delete duplicate translation 2019-12-19 20:14:24 +00:00
5c05bd2562 If a result is for a shared file, display it as Downloaded 2019-12-19 20:12:02 +00:00
9df1d043e4 do not initialize the update client if running as a plugin 2019-12-19 18:35:44 +00:00
6ea1a15641 do not initialize the update client if running as a plugin 2019-12-19 18:30:07 +00:00
c0575facec add Downloaded string 2019-12-19 13:12:01 +00:00
09168844e0 css tweaks from zzz 2019-12-19 12:53:51 +00:00
e21d482393 Release 0.6.8 2019-12-19 06:04:18 +00:00
f5fc3e40c2 add incomplete translations 2019-12-18 11:26:06 +00:00
796a0138fa change Trust Users title for clearer translation; add headers for the two tables 2019-12-18 11:25:36 +00:00
505b4ddb06 comment clairfying verb/noun for Pause and Query 2019-12-18 10:57:56 +00:00
a35216ff56 some translations 2019-12-18 08:23:56 +00:00
fba92fe9b9 missing strings 2019-12-17 20:21:01 +00:00
1cc511b0ae initialize root node in the init function so that it can be translated 2019-12-17 20:19:38 +00:00
fa94c8ebfa persist files after unsharing and pause to let event propagate 2019-12-17 17:24:50 +00:00
88b68a3c5c move controls to the right of the tree nodes 2019-12-17 17:14:30 +00:00
b3e0d2ee7a display tree in order it arrives from servlet 2019-12-17 15:38:24 +00:00
ce293cbda8 sort file tree servlet side 2019-12-17 15:28:49 +00:00
3abc617e9f css changes from zzz 2019-12-17 14:25:42 +00:00
67ee634f20 separate link text for a single certificate 2019-12-17 13:51:34 +00:00
503d54927f fix directories with special characters in them in file tree view 2019-12-17 13:22:31 +00:00
5788329e1a add ability to set css class to sortable tables. Make certificates table certificates class 2019-12-17 12:52:33 +00:00
f0ffc68122 more strings 2019-12-17 12:43:49 +00:00
3d710cebe5 no <pre> if there is no reason 2019-12-17 12:39:35 +00:00
7d67573c92 rename 2019-12-17 11:40:47 +00:00
3acc676448 table to div.right 2019-12-17 11:38:40 +00:00
2bf03b6b84 use div.centercomment for trust comments 2019-12-17 10:30:07 +00:00
b8ba6df4d5 link to BrowseHost page 2019-12-17 10:21:48 +00:00
9fa7fa07b4 whitespace between links 2019-12-17 10:19:47 +00:00
1c7253ea0a more _t 2019-12-17 10:16:17 +00:00
d947ad2997 more table->span.right 2019-12-17 10:15:00 +00:00
dd0bd6f5f8 use pre.comment for trust reasons 2019-12-17 07:09:37 +00:00
f05b6d0b40 word-wrap pre.comment pt.2 2019-12-17 07:05:24 +00:00
906c69a482 word-wrap pre.comment 2019-12-17 07:04:13 +00:00
5375b7aec0 add class to <pre> blocks 2019-12-16 22:25:10 +00:00
ea5da2431a remove ;, thanks to jshint and zzz 2019-12-16 21:35:57 +00:00
14b3a9ac9e add more js strings 2019-12-16 21:14:44 +00:00
40bbef4583 remove unneeded files 2019-12-16 20:51:22 +00:00
f811653247 remove unnecessary strings from Util._x 2019-12-16 20:03:30 +00:00
f321000071 sort tables by default 2019-12-16 19:26:24 +00:00
6eb85283cd sort tables by default 2019-12-16 19:19:09 +00:00
2973759cd9 sort tables by default 2019-12-16 19:16:06 +00:00
fe945a9941 sort tables by default 2019-12-16 19:12:48 +00:00
5f7e949310 clear tables when closing current search 2019-12-16 19:07:01 +00:00
11edb2cb3c convert tables to div.right 2019-12-16 18:48:28 +00:00
ff1f801155 convert tables to div.right 2019-12-16 18:43:31 +00:00
0a98083c64 convert tables to div.right 2019-12-16 18:39:53 +00:00
75b2852f6e convert table to div.right for links 2019-12-16 18:30:39 +00:00
5774cdee94 move comment box to the center 2019-12-16 18:11:30 +00:00
2b0f4e52ca ellipsis on overflow, input alignment fixes from zzz 2019-12-16 17:51:10 +00:00
1d20dc917b make the comment box 50% of available space in table view 2019-12-16 17:28:38 +00:00
63e3b3710c bottom table id based on view type 2019-12-16 16:22:51 +00:00
0878b89082 different ids for the top table based on view type 2019-12-16 16:05:52 +00:00
fecf0ecae8 put trusted and distrusted tables on top of one another 2019-12-16 14:56:54 +00:00
fec8d4ef9f Done->Downloaded Pieces 2019-12-16 14:51:22 +00:00
067ac8582a Lists->Subscriptions 2019-12-16 14:47:52 +00:00
31cac25a23 remove fetch link, make the file name a link 2019-12-16 14:43:38 +00:00
6bcc44e01e align comment textarea to the right 2019-12-16 14:32:44 +00:00
31652b34d7 column sizing, tags, other changes from zzz 2019-12-16 14:24:27 +00:00
41a15fc7d5 clear Speed and ETA columns for finished downloads 2019-12-16 14:22:12 +00:00
da3d7d7a50 herf->href 2019-12-16 13:43:16 +00:00
3a079d9f21 expand root by default, expand until there is more than one child 2019-12-16 13:16:39 +00:00
ba0c85fe07 do not show unshare/comment/certify links for directories that are not shared 2019-12-16 13:01:56 +00:00
ecb2283886 comment out help section 2019-12-16 09:18:27 +00:00
cf9a18cee5 style init page 2019-12-16 05:11:20 +00:00
982a93a04b get rid of static headers in trust list view 2019-12-16 04:58:40 +00:00
58137d11d1 space out trust links in search view 2019-12-16 01:49:59 +00:00
d87bec927d space out links in trust users view 2019-12-16 01:32:00 +00:00
dc8dd96495 space out links in trust lists view 2019-12-16 01:26:43 +00:00
add9fb6feb revision is an integer 2019-12-16 01:15:17 +00:00
c500e95ab6 register for correct event 2019-12-16 01:05:31 +00:00
477c3285d2 do not display empty files table 2019-12-15 23:36:01 +00:00
1f5b112bfe fix distrusting 2019-12-15 23:31:07 +00:00
b0d09853e4 pause after publishing all trust events 2019-12-15 23:28:00 +00:00
b96d997037 do not show empty tables 2019-12-15 23:22:40 +00:00
a631ec1e14 do not display empty or stale tables in trust list view 2019-12-15 23:14:24 +00:00
62a06bc891 do not show empty or stale tables after closing browses 2019-12-15 22:46:42 +00:00
3534b23194 correct string 2019-12-15 22:20:29 +00:00
c561ae9140 get possible sources from browse host 2019-12-15 22:19:26 +00:00
5926457eb5 send redirect after manual browse input 2019-12-15 22:14:09 +00:00
37c93e352b Make Downloading a link 2019-12-15 19:39:07 +00:00
be8fecda39 Change Downloading to a link 2019-12-15 19:29:47 +00:00
7ec6257ac0 implement closing browses 2019-12-15 19:02:51 +00:00
c4ea58c330 add Sources column to group-by-file view 2019-12-15 18:47:52 +00:00
a482fe5c93 turn browse link into browsing link 2019-12-15 18:40:19 +00:00
2ee84848c4 make Browsing a link to the browse page 2019-12-15 16:55:11 +00:00
e29d7f6872 do not display active searches table if it's empty 2019-12-15 16:47:07 +00:00
5ded824ef2 display tables side by side 2019-12-15 16:42:16 +00:00
c607560cb8 space between trust action links 2019-12-15 16:42:03 +00:00
8b341bb125 tell the user the directories will be created 2019-12-15 16:17:56 +00:00
6bc5a9075b rewrite welcome jsp to a servlet, add sanity check of inputs 2019-12-15 16:16:11 +00:00
6b1d2bc5ce sanitize the i2p tunnel settings 2019-12-15 15:36:03 +00:00
0cbbaf6a63 localized error messages 2019-12-15 15:33:15 +00:00
3363b99675 sanitize integer and file input 2019-12-15 15:13:44 +00:00
4ab4785539 display errors on invalid config input 2019-12-15 15:06:18 +00:00
e595fa97e8 change some strings for easier translation 2019-12-15 14:53:35 +00:00
65a7088463 can squeeze a few more characters 2019-12-15 13:39:20 +00:00
2d5bd653c1 do not display number of results if it's zero 2019-12-15 13:35:16 +00:00
a864343c05 get rid of senders and results columns, use ellipsis for very long search strings 2019-12-15 13:29:03 +00:00
696b348469 link to translation instructions 2019-12-15 12:41:08 +00:00
b08333c5ea download details view 2019-12-15 11:34:04 +00:00
0cf368c1af uploads icon 2019-12-15 10:00:26 +00:00
62ab957892 clear finished uploads link 2019-12-15 08:57:14 +00:00
2b9e722165 clear finished downloads link 2019-12-15 08:29:55 +00:00
8cf4b23762 ability to stop a search 2019-12-15 07:58:16 +00:00
1285c68521 uploads page 2019-12-15 03:26:55 +00:00
daa9e0bafc servlet side of uploader page 2019-12-15 02:45:14 +00:00
8efd9c2c88 headers for the sections 2019-12-14 21:42:42 +00:00
918549f164 hook up to translations 2019-12-14 21:18:43 +00:00
e30a4666cb wip on configuration page 2019-12-14 20:49:54 +00:00
26167abc08 fix connections count on settings page 2019-12-14 20:34:57 +00:00
93f7c67f37 wip on configuration page 2019-12-14 20:27:13 +00:00
f9a0a5e08a wip on settings page 2019-12-14 19:30:11 +00:00
d8ae275df2 remove debug println 2019-12-14 19:25:29 +00:00
fce879be5d hook up configuration page, under construction 2019-12-14 19:02:15 +00:00
0b58e22714 start work on configuration page 2019-12-14 17:47:52 +00:00
dd230c4dfc automatic resume of failed downloads 2019-12-14 14:16:29 +00:00
fba0b001c0 pause/resume/retry links 2019-12-14 14:02:25 +00:00
6978c7b992 switch certify and comment links 2019-12-14 10:17:19 +00:00
7355e76e1b add fetch link to shared file table view 2019-12-14 09:09:03 +00:00
5147cf21a0 hook up the downloaded content servlet 2019-12-13 23:57:51 +00:00
e8dd7d710d pause to give a chance to the event to propagate 2019-12-13 13:10:11 +00:00
fc9114eaa5 use Collator for comparing strings 2019-12-13 12:21:11 +00:00
20b7104c41 wrong formatting for ETA and progress 2019-12-13 11:37:20 +00:00
570616951a sortable certificate table, add extra parameter to Table object 2019-12-13 11:27:29 +00:00
e075bfac55 auto-refresh the files table if revision changed 2019-12-13 08:28:00 +00:00
b6411a555c hide links on root node 2019-12-13 08:27:35 +00:00
d395475727 sortable shared files table 2019-12-13 08:04:19 +00:00
8ae0a16b8a sortable trust list tables 2019-12-13 02:23:42 +00:00
38fcdfc97a sorting of trust subscriptions table 2019-12-13 00:04:10 +00:00
a0fb07cf99 Link helper class 2019-12-12 22:31:42 +00:00
3747f9a5d5 sortable tables on trust users page 2019-12-12 21:43:54 +00:00
3a738f8f62 sorting downloads table 2019-12-12 17:26:56 +00:00
ca56363438 server-side of downloads sorting support 2019-12-12 13:19:25 +00:00
e06cb05e2a fix glitch in sorting when new results arrive 2019-12-12 01:16:42 +00:00
8ab2dd7900 sort all tables on search page 2019-12-12 00:44:49 +00:00
26116d313a avoid an exception 2019-12-11 22:34:16 +00:00
738f177d6c update certificate hooks to new architecture 2019-12-11 22:28:54 +00:00
62c4579bbd preserve expanded comment state during updates 2019-12-11 21:47:03 +00:00
18d84685ec wip on rewriting search page for sortable tables. Some features do not yet work 2019-12-11 20:45:12 +00:00
c05a7a021c table styling and caret on the file tree from zzz 2019-12-11 14:40:49 +00:00
a9935eba62 wip on restructuring search xhr 2019-12-11 14:38:42 +00:00
e3d80bf809 remove fonts 2019-12-11 14:38:05 +00:00
a59a1d3f30 sort active searches 2019-12-11 10:55:51 +00:00
37ed75a3e8 sort table of active browses 2019-12-11 07:42:42 +00:00
cd4b600ba2 working sorting of the browse host results 2019-12-10 23:56:39 +00:00
fcd6dbcfbd wip on sortable tables 2019-12-10 23:24:11 +00:00
f3ab15bd74 certificates in browse host page 2019-12-10 21:33:36 +00:00
cddaad0f29 move certificate code in a separate file 2019-12-10 20:33:38 +00:00
ecb597e0a0 preserve shown/hidden certificate comment state 2019-12-10 17:20:10 +00:00
ec2a934f73 wip on show/hide certificate comments 2019-12-10 16:54:21 +00:00
e1d630fdee wip on showing comments in certificates 2019-12-10 16:13:59 +00:00
5807672503 proper ignore pattern 2019-12-10 16:04:42 +00:00
2fadb314d3 css and layout changes from zzz 2019-12-10 15:35:54 +00:00
ec5c15ff64 importing of certificates 2019-12-10 15:34:51 +00:00
c169a7613f wip on importing certificates 2019-12-10 14:59:30 +00:00
0f762968ae show fetched certificates in a table 2019-12-10 14:32:35 +00:00
8e6517e7d8 content serving servlet, thx to zzz 2019-12-10 12:50:38 +00:00
6946bff7f9 hook up periodic certificate update function 2019-12-10 12:47:56 +00:00
37dcedb99b remove .orig 2019-12-10 12:26:00 +00:00
afb92b0e4e translation updates, images, thanks zzz 2019-12-10 12:24:56 +00:00
7c39dff34f wip on rendering certs table 2019-12-10 12:21:20 +00:00
e41c122d2d show/hide links for certificates in group-by-sender view 2019-12-10 08:53:27 +00:00
117c5eaf67 wip on showing certificates 2019-12-10 08:12:45 +00:00
10fab2b47f certification in view by table 2019-12-09 23:21:24 +00:00
3f71df3d29 certification support in tree view 2019-12-09 23:00:40 +00:00
813e211200 send certified status to the UI 2019-12-09 17:22:30 +00:00
1adb130fba ability to certify directories 2019-12-09 17:04:11 +00:00
f69d4027db ability to certify files 2019-12-09 16:19:45 +00:00
e0d006ec69 translate more strings 2019-12-09 15:39:18 +00:00
81d8af57ed Translation infrastructure, thanks to zzz 2019-12-09 15:17:13 +00:00
42c48a8e37 certificate backend 2019-12-09 14:26:39 +00:00
3b1349b643 ability to force refresh lists 2019-12-09 10:06:56 +00:00
0250ea329c fix some nevers and nulls 2019-12-09 09:54:10 +00:00
b722c64ad8 comments in trust actions 2019-12-09 09:51:02 +00:00
effa3b567e persist subscription lists 2019-12-09 09:20:07 +00:00
64f198d599 fix live updating on trust action 2019-12-09 09:06:45 +00:00
131b2defbb trust actions 2019-12-09 09:02:41 +00:00
df5aab67ac hook up lists page 2019-12-09 08:22:42 +00:00
fdc030904c wip on trust lists 2019-12-09 08:19:55 +00:00
2a4fae8de4 wip on trust lists 2019-12-09 07:47:47 +00:00
662b065116 wip on trust subscriptions 2019-12-09 07:37:04 +00:00
300938fa44 wip on trust lists page 2019-12-09 07:04:35 +00:00
086e27876d shut down more services explicitly 2019-12-09 05:38:41 +00:00
247c62bfb4 hook up trust page 2019-12-09 05:23:40 +00:00
a13315c324 describe the textbox 2019-12-09 04:38:14 +00:00
65f40ef23a trust/neutral/distrust links 2019-12-09 04:32:35 +00:00
96a611ff78 xhr fixes 2019-12-09 00:09:55 +00:00
0f4119b74f submit trust functionality 2019-12-08 23:47:49 +00:00
6847329093 trust buttons, submitting doesn't work yet 2019-12-08 23:25:06 +00:00
9d2bcf70c7 display trust status in results 2019-12-08 22:30:38 +00:00
aa33709f04 fix display of query 2019-12-08 21:08:49 +00:00
eacaedaf3d automatically update active browse if the revision has changed 2019-12-08 21:05:29 +00:00
f9c428cfcd update comment indexing 2019-12-08 20:48:15 +00:00
aa1ede46d2 Redesign the XHR architecture by splitting the requests. Separate requests are issued for the status table, then a request is triggered when a user clicks on a search. 2019-12-08 20:41:54 +00:00
3c43244631 wip on trust users view 2019-12-08 18:11:12 +00:00
b468a6f19b update web.xml 2019-12-08 17:39:04 +00:00
cfdc750ac0 post method 2019-12-08 17:36:30 +00:00
6f8b006227 post method 2019-12-08 17:35:53 +00:00
3f4bf986f3 remove stray orig file, update gitignore 2019-12-08 16:55:47 +00:00
bef1033e12 plugin must compile with java 8 2019-12-08 16:41:45 +00:00
13061d60a4 add local status to trust list xml 2019-12-08 15:13:30 +00:00
5c6917a7e6 wip on trust views 2019-12-08 14:57:21 +00:00
2ec15cfbbc remove jsp from urls, thanks to zzz 2019-12-08 13:45:26 +00:00
1325a8dc65 resolve conflicts, fix quotes, thanks zzz 2019-12-08 13:03:21 +00:00
b5d8fcf25b missed tx/config 2019-12-08 12:51:25 +00:00
c22ff0678e mark script executable 2019-12-08 12:44:11 +00:00
07051b813a translation infrastructure, thanks to zzz 2019-12-08 12:41:45 +00:00
5c22af6576 add link to sidebar 2019-12-08 12:33:46 +00:00
c3e1298ea3 browse links from search results 2019-12-08 12:31:02 +00:00
949b616fdd fix xml, placeholders for browse links 2019-12-08 11:43:21 +00:00
2b1d95e2ef pass sender's b64 and browse status from endpoint 2019-12-08 11:35:30 +00:00
3d967da110 move browses table to top of page 2019-12-08 11:17:13 +00:00
66fde32b64 comments support in browse host 2019-12-08 10:44:18 +00:00
80a89a5ac0 download functionality 2019-12-08 09:38:34 +00:00
c59e038c2a wip on browse host 2019-12-08 07:48:59 +00:00
844bd8fd6e comments in shared files are encoded 2019-12-08 00:26:17 +00:00
7d9ebb5b0b server side of browse host 2019-12-07 23:35:16 +00:00
7fd7444dbf unshare directories to make sure files do not end up in the negative tree 2019-12-07 20:48:45 +00:00
13af6cce22 stray println 2019-12-07 20:37:24 +00:00
458dbec5fd display a refresh link if the table needs updating 2019-12-07 20:23:22 +00:00
2137d6d30b comments in table view 2019-12-07 19:34:20 +00:00
b28de0c119 add unshare link 2019-12-07 19:16:48 +00:00
0fd4695b7c wip on table view 2019-12-07 18:53:32 +00:00
74dddc4da4 wip on table view 2019-12-07 18:07:00 +00:00
8bff987d30 implement adding comments to files 2019-12-07 17:19:13 +00:00
de8684bafc fix multiline comments by not adding <br> tags in the servlet and using <pre> tag in the browser 2019-12-07 15:15:45 +00:00
905f559aa9 proper <br /> tags 2019-12-07 14:23:50 +00:00
c7f57c0b15 update sidebar, add sidebar to shared files, <br> in comments, thanks to zzz 2019-12-07 13:36:33 +00:00
0f0f46f425 rename Files.jsp 2019-12-07 13:10:17 +00:00
d6a3c8b24c re-add zzz's changes to FilesServlet 2019-12-07 13:04:31 +00:00
8c661ca1ae unescape file names, this fixes unsharing of files with html characters 2019-12-07 12:59:43 +00:00
f579c8754f more sidebar work thanks to zzz 2019-12-07 12:18:01 +00:00
5c17536683 unsharing of directories 2019-12-07 12:14:49 +00:00
8536353c26 unshare individual files 2019-12-07 11:20:56 +00:00
84375c0201 fix typo and collapsing 2019-12-07 10:16:28 +00:00
9c0c187a18 base64 encode the div ids to account for special characters in names 2019-12-07 10:13:17 +00:00
8ae735e5c0 get tree structure to display, no collapsing yet 2019-12-07 09:31:56 +00:00
8224dda3fd sidebar, servlet and styling improvements from zzz 2019-12-06 18:26:44 +00:00
c852d7474e base64 encoding function 2019-12-06 17:25:18 +00:00
71685d2052 clear hashing span when not hashing 2019-12-06 17:20:41 +00:00
e57e513ca1 wip on sharing files 2019-12-06 17:02:40 +00:00
aa4fb14540 wip on sharing and unsharing of files server-side 2019-12-06 15:58:02 +00:00
5f74abc944 hook up files servlet and file manager 2019-12-06 13:44:15 +00:00
c4135389a4 wip on shared files display page 2019-12-06 13:16:32 +00:00
a6e0834722 add a single-level list traversal of the tree 2019-12-06 12:47:08 +00:00
bc628b9c00 layout and escaping, thanks zzz 2019-12-06 11:02:53 +00:00
9b2669a8b8 update to new api 2019-12-06 10:51:35 +00:00
a0f70f7677 add traversal of the file tree 2019-12-06 10:51:07 +00:00
23b2c912e2 genericize file tree 2019-12-06 10:08:27 +00:00
ecfd4180c0 update test 2019-12-06 10:07:32 +00:00
42489ba6b2 add support for showing/hiding comments 2019-12-06 01:34:35 +00:00
61207f893d cancelled downloads do not count as downloading 2019-12-05 22:11:39 +00:00
4e32359718 refresh downloads on cancel 2019-12-05 22:08:35 +00:00
8d4af48eca cancel downloads via ajax too 2019-12-05 21:50:06 +00:00
693f63534d download via ajax for group-by-file view as well 2019-12-05 21:37:17 +00:00
b057e848d0 use ajax for starting downloads 2019-12-05 21:18:29 +00:00
0114224d1f various html fixes, version the js, thanks zzz 2019-12-05 13:40:37 +00:00
beab2be713 null checks on unitialized core, html escaping, move scriptst to <head>, thanks zzz 2019-12-05 12:19:10 +00:00
edd4a1ff4b move download js into a separate file 2019-12-05 11:31:19 +00:00
85814b7544 move search javascript into a separate file 2019-12-05 11:24:31 +00:00
d46fbd66f0 move connection count into a separate js file, thanks zzz 2019-12-05 10:38:24 +00:00
06bd9c80e8 move connection count refreshing into the header 2019-12-04 23:34:21 +00:00
54b8628435 convert download servlet to xml and page to ajax 2019-12-04 23:15:50 +00:00
b37a548771 Some refactoring thanks to zzz plus some wip on migrating downloads page to an xml-based servlet 2019-12-04 22:12:34 +00:00
a14689acff set debug parameter to javac task 2019-12-04 19:37:58 +00:00
a73bc956bf set the plugin icon 2019-12-04 19:23:43 +00:00
d595a768b8 put images in images/ 2019-12-04 19:15:07 +00:00
0fd6421fae bundle images in war 2019-12-04 19:03:05 +00:00
6e9a36461a get mwClient from application scope 2019-12-04 19:02:51 +00:00
d115f54812 copy icon from i2p source tree 2019-12-04 19:00:48 +00:00
f627f661f2 add bote's css and images 2019-12-04 18:57:29 +00:00
0e7ec3dfb3 move css to its own file 2019-12-04 11:27:08 +00:00
0188bd34a9 add download buttons 2019-12-04 10:55:18 +00:00
a2becfa6e2 implement grouping by file 2019-12-04 07:45:51 +00:00
ea32af9b91 align tables 2019-12-04 05:17:16 +00:00
c74c26e4c6 construct a tree structure to match XML received from servlet; populate tables from it 2019-12-04 02:48:28 +00:00
382e21225b display list of senders 2019-12-03 23:59:51 +00:00
81c406cbf6 refresh search results and connection count with ajax 2019-12-03 23:00:39 +00:00
d9eb46d65c update min java version for plugin 2019-12-03 18:17:55 +00:00
dadfed20f1 proper plugin build number 2019-12-03 16:25:38 +00:00
6dad29a772 instructions for building the plugin 2019-12-03 16:05:26 +00:00
884253fe29 easier running instructions 2019-12-03 12:07:39 +00:00
a5eccbdc2b sleep a bit to give event chance to propagate 2019-12-03 06:04:14 +00:00
d0318e3e83 display direct and possible sources. Pass possible sources to core 2019-12-03 06:00:56 +00:00
d1c308f118 access mwClient from the application context 2019-12-02 14:12:23 +00:00
3871170e44 show number of connections 2019-12-01 02:51:00 +00:00
95dd5c4a7c downloads display, starting and stopping 2019-11-30 23:34:59 +00:00
0bff4b55a5 format the results as table, add download buttons 2019-11-30 21:55:41 +00:00
a2022415c2 add display of search results grouped by sender 2019-11-30 19:54:50 +00:00
2b8bd8144f basic display of how many senders and results have arrived 2019-11-30 19:09:55 +00:00
7bf520ac8c skeleton of search manager 2019-11-30 18:16:25 +00:00
ad8983e889 wait for client manager to load before connecting 2019-11-30 17:32:02 +00:00
d0b62af32e change url to point to servlet 2019-11-30 17:31:43 +00:00
bc8e259974 update readme for web ui and version 2019-11-30 15:43:04 +00:00
ff0a4661fd offload start to a thread, display wait page while the tunnel is opening 2019-11-30 14:56:04 +00:00
9151df6816 kill i2p session on shutdown 2019-11-30 14:27:40 +00:00
9c0878408b redirect to I2P log system 2019-11-30 14:07:58 +00:00
61baa53076 _logManager cannot be set on RouterContexts (i.e. when running as plugin) 2019-11-30 13:26:20 +00:00
b2841ee9ab fix redirects 2019-11-30 13:20:48 +00:00
9edea17fb7 switch to using a servlet instead of bean 2019-11-30 13:07:47 +00:00
ac17618f0c fix incomplete location setting 2019-11-30 10:50:58 +00:00
e94ed4eafa init nickname and download locations 2019-11-30 10:22:19 +00:00
8c33a5e62f hook up the mw client app with the jsp 2019-11-30 08:28:22 +00:00
f9f1017e5b initialize core if nickname etc. is provided 2019-11-30 06:50:44 +00:00
5d2d831b9e pass MW home to the client 2019-11-30 06:21:32 +00:00
562d9a0f4a move i2p core dependency one level down, exclude core dependencies from plugin 2019-11-30 03:44:57 +00:00
b981f9199b pass version to MW client app and get it to run 2019-11-30 03:16:10 +00:00
efef0f3734 include a servlet as well as pre-compiled jsps 2019-11-29 18:00:32 +00:00
cd0b860210 skeleton of client app 2019-11-29 17:02:15 +00:00
9cb0655cfa get a buildable i2p plugin for mw 2019-11-29 16:49:44 +00:00
3775f28af7 add jsp-based webui 2019-11-29 16:40:02 +00:00
c33b824871 remove grails webui 2019-11-29 16:37:57 +00:00
cf396b739e ability to chat from browse window 2019-11-29 03:41:59 +00:00
631963f43c browse host by full nickname 2019-11-29 02:26:34 +00:00
06cedb4f41 add buttons to copy short and full nickname to clipboard 2019-11-29 02:19:47 +00:00
7a0c60a164 exit if user refuses to choose a nickname 2019-11-28 16:39:38 +00:00
4c038ad932 set the geoip.dir property to load geoip 2019-11-27 16:00:56 +00:00
f6dd38685a display country and strictness in I2P status 2019-11-27 15:38:51 +00:00
2eab0f0567 make the chat monitor a separate frame so that it does not dissappear when MW is minimized 2019-11-26 18:55:20 +00:00
8fedc0c605 Release 0.6.7 2019-11-26 09:55:10 +00:00
5831b06842 chat room monitor tool 2019-11-26 09:43:53 +00:00
57d5b5f386 do not send /LEAVE messages when leaving private chats 2019-11-26 05:31:22 +00:00
c0f6b1ed73 do not rejoin console or private chats, fix NPE when disconnecting with private window open 2019-11-26 05:28:24 +00:00
f4cd1c30cd Do not remove connection on distrust so that disconnect can be processed correctly 2019-11-26 05:00:55 +00:00
6b717f560e file hashing 2019-11-23 20:28:29 +02:00
e8a3db76bb wip on architecture doc 2019-11-23 20:15:45 +02:00
5acf7f2953 add clear button 2019-11-20 18:47:14 +00:00
e760e9f600 add option to select chat server welcome message 2019-11-19 09:02:42 +00:00
8a47972b10 proper group name for manual rejoin 2019-11-19 03:37:35 +00:00
f8e0c9524e make text input fields longer 2019-11-18 10:03:01 +00:00
919aeaaed5 preserve chat box contents across openings of the chat window 2019-11-18 09:55:11 +00:00
9474512cbd default to /SAY command 2019-11-18 09:44:05 +00:00
8c50f6c6d6 proper model update 2019-11-18 09:33:25 +00:00
01ee7209c8 clear members list on server disconnect 2019-11-18 09:31:25 +00:00
ff7c4eae28 stop local server if tab is closed 2019-11-18 09:06:29 +00:00
9373d58b53 limit the time a header read can take 2019-11-18 09:00:11 +00:00
df71ade69f formatting 2019-11-18 08:46:28 +00:00
2ed29be072 selected component can be null when closing a tab 2019-11-17 22:41:31 +00:00
a398ab7d4b indentation 2019-11-17 18:38:53 +00:00
a0125e7195 document chat protocol 2019-11-17 18:37:30 +00:00
cb9a1cfff6 remove stray import 2019-11-17 15:05:29 +00:00
445e73521a more links 2019-11-17 13:35:40 +00:00
7bdc922d2c add links 2019-11-17 13:32:11 +00:00
0c40c8f269 Release 0.6.6 2019-11-16 17:12:21 +00:00
681ddb99a2 add ability to say something in a room 2019-11-16 17:10:21 +00:00
5dff319746 prevent starting chat server more than once. Implement chat console in the cli 2019-11-16 16:40:07 +00:00
57c4a00ac6 do not call disconnect() unless connecting/ed. This prevents trying to connect after closing the tab 2019-11-16 15:25:23 +00:00
286a0a8678 redo locking around chat client state 2019-11-16 14:41:47 +00:00
17eff7d77f toString 2019-11-16 00:25:50 +00:00
2e22369ce0 set flag before submitting to threadpool 2019-11-15 23:12:17 +00:00
15c59b440f flush outputstream on chat connect failures or rejections 2019-11-15 21:52:35 +00:00
8fb015acbf sort browse host as well 2019-11-15 14:59:52 +00:00
f7b11c90fd sort results from search 2019-11-15 14:49:15 +00:00
df93a35062 wip on sorting 2019-11-15 14:23:00 +00:00
ecb19a8412 do not update badge if the room is a console 2019-11-15 13:50:13 +00:00
b1e5b40800 update minimum JDK to 9, version bump 2019-11-15 13:18:34 +00:00
daa3a293f2 new messages update taskbar badge 2019-11-15 13:15:27 +00:00
907264fc67 enable/disable chat and browse from trusted pane buttons 2019-11-15 02:15:56 +00:00
c6becb93dc enable/disable say field when not connected 2019-11-15 01:57:48 +00:00
2954bd2f1a smart scrolling the chat text area 2019-11-15 01:44:54 +00:00
35322d2c15 fetch group by name,add sequential download checkbox to browse view 2019-11-14 12:40:40 +00:00
9f6a7eb368 make sure browse window works from every parent group 2019-11-14 11:04:38 +00:00
306 changed files with 48275 additions and 25751 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
# Dot directories
.gradle/
.idea/
.git/
# Build directories
build/
**/build/
# We execute COPY . .
# Modifying these files would unnecessarily invalidate the build context
Dockerfile

2
.gitignore vendored
View File

@ -4,3 +4,5 @@
.gradle
.project
.classpath
**/*.rej
**/*.orig

9
.tx/config Normal file
View File

@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
lang_map = he: iw, id: in, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
[I2P.MuWire]
file_filter = webui/locale/messages_<lang>.po
source_file = webui/locale/messages_en.po
source_lang = en
minimum_perc = 10

64
Dockerfile Normal file
View File

@ -0,0 +1,64 @@
FROM jlesage/baseimage-gui:alpine-3.10-glibc
# Docker image version is provided via build arg.
ARG DOCKER_IMAGE_VERSION=unknown
# JDK version
ARG JDK=9
# Important directories
ARG TMP_DIR=/muwire-tmp
ENV APP_HOME=/muwire
# Define working directory.
WORKDIR $TMP_DIR
# Put sources into dir
COPY . .
# Install final dependencies
RUN add-pkg openjdk${JDK}-jre
# Build and untar in future distribution dir
RUN add-pkg --virtual openjdk${JDK}-jdk \
&& ./gradlew --no-daemon clean assemble \
&& mkdir -p ${APP_HOME} \
# Extract to ${APP_HOME and ignore the first dir
# First dir in tar is the "MuWire-<version>"
&& tar -C ${APP_HOME} --strip 1 -xvf gui/build/distributions/MuWire*.tar \
# Cleanup
&& rm -rf "${TMP_DIR}" /root/.gradle /root/.java \
&& del-pkg openjdk${JDK}-jdk
WORKDIR ${APP_HOME}
# Maximize only the main/initial window.
RUN \
sed-patch 's/<application type="normal">/<application type="normal" title="MuWire">/' \
/etc/xdg/openbox/rc.xml
# Generate and install favicons.
RUN \
APP_ICON_URL=https://github.com/zlatinb/muwire/raw/master/gui/griffon-app/resources/MuWire-128x128.png && \
install_app_icon.sh "$APP_ICON_URL"
# Add files.
COPY docker/rootfs/ /
# Set environment variables.
ENV APP_NAME="MuWire" \
S6_KILL_GRACETIME=8000
# Define mountable directories.
VOLUME ["$APP_HOME/.MuWire"]
VOLUME ["/incompletes"]
VOLUME ["/output"]
# Metadata.
LABEL \
org.label-schema.name="muwire" \
org.label-schema.description="Docker container for MuWire" \
org.label-schema.version="$DOCKER_IMAGE_VERSION" \
org.label-schema.vcs-url="https://github.com/zlatinb/muwire" \
org.label-schema.schema-version="1.0"

View File

@ -2,13 +2,13 @@
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.6.8 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
The current stable release - 0.6.2 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
### Building
## Building
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew clean assemble
@ -19,27 +19,83 @@ If you want to run the unit tests, type
./gradlew clean build
```
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
### Running the GUI
## Running the GUI
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z-all.jar` in a terminal or command prompt.
Type
```
./gradlew gui:run
```
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
[Default I2CP port]\: `7654`
### Running the CLI
## Running the CLI
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
The CLI is under active development and doesn't have all the features of the GUI.
### Web UI
## Running the Web UI / Plugin
If you are a Grails/Scala/JRuby/Kotlin developer and are interested in building a Web UI for MuWire, please get in touch. The MuWire core is written in Groovy and should be easy to integrate with any JVM-based language.
There is a Web-based UI under development. It is intended to be run as a plugin to the Java I2P router. Instructions how to build it are available at the wiki [Plugin] page.
### GPG Fingerprint
## Docker
The Docker image is based on the wonderful work in [jlesage/docker-baseimage-gui].
You can refer to it for environment variables to pass to the container.
If you don't want to use the image on dockerhub, build an image yourself.
```bash
MUWIRE_VERSION=`awk -F "=" '/^version/ { gsub(" ","") ; print $2}' gradle.properties`
docker build -t muwire:latest,muwire:${MUWIRE_VERSION} .
```
**Necessary configuration**
Since MuWire will be running in a container, it won't have direct access to the host's localhost.
By default, it will be configured to use `172.17.0.1` as the target host.
You'll need to open the I2CP port on that interface.
If you're running I2P on the localhost, navigate to http://localhost:7657/configi2cp and make the necessary changes.
![i2cp_config.png]
Should you be using a different interface write an `i2p.properties` and then put that into the shared docker volume.
Example configuration file:
```properties
i2cp.tcp.host=112.13.0.1
```
**Running**
```bash
docker run \
-p 5800:5800 \
-v config:/muwire/.MuWire \
-v incompletes:/incompletes \
-v output:/output \
--name muwire \
zlatinb/muwire
```
You will then be able to access the muwire GUI over a browser at http://localhost:5800
**Options**
| Option | Description |
|--------------|--------------------------------------------|
|`-v config:/muwire/.MuWire`| This is where the `i2p.properties` and possibly other config should go |
|`-v incompletes:/incompletes`| The `/incompletes` volume should be used to store MuWire's **incomplete** download/upload data \*|
|`-v output:/output`| The `/output` volume should be used to store MuWire's download/upload data |
## Translations
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
## GPG Fingerprint
```
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
@ -49,3 +105,12 @@ You can find the full key at https://keybase.io/zlatinb
[Default I2CP port]: https://geti2p.net/en/docs/ports
[Wiki]: https://github.com/zlatinb/muwire/wiki
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
[I2P Github]: https://github.com/i2p/i2p.i2p
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
[i2cp_config.png]: ./images/i2cp_config.png
[muwire_incompletes.png]: ./images/muwire_incompletes.png
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui

42
TODO.md
View File

@ -1,8 +1,6 @@
# TODO List
Not in any particular order yet
### Big Items
### Network
##### Bloom Filters
@ -12,15 +10,33 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability
##### Web UI, REST Interface, etc.
### Core
Basically any non-gui non-cli user interface
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items
* Wrapper of some kind for in-place upgrades
* Metadata parsing and search
* Automatic adjustment of number of I2P tunnels
* Persist trust immediately
* Check if user-selected download and incomplete locations exist and are writeable
* Enum i18n
* Ability to share trust list only with trusted users
* Confidential files visible only to certain users
* Public Feed feature
### Chat
* echo "unknown/innappropriate command" in the console
* break up lines on CR/LF, send multiple messages
* Style timestamps and persona names
* enforce # in room names or ignore it
* auto-create/join channel on server start
* jump from notification window to room with message
### Swing GUI
* I2P Status panel - display message when connected to external router
* Search box - left identation
### Web UI/Plugin
* HTML 5 media players
* Minimal dependency (break up groovy-all.jar)
* Remove versions from jar names
* Security: POST nonces, CSP headers

View File

@ -2,8 +2,9 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
compile 'org.codehaus.groovy:groovy-all:2.4.15'
compile 'org.codehaus.groovy:groovy:2.4.15'
compile 'org.codehaus.groovy:groovy-jsr223:2.4.15'
compile 'org.codehaus.groovy:groovy-json:2.4.15'
}
compileGroovy {

View File

@ -72,4 +72,27 @@ class BrowseModel {
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
Collections.sort(l, chosen)
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
l.each { e ->
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment, e.certificates)
}
}
}

View File

@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
}
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...", {sort()})
Button closeButton = new Button("Close",{
model.unregister()
close()
})
contentPanel.addComponent(closeButton, layoutData)
buttonsPanel.addComponent(sortButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
}
@ -120,4 +126,11 @@ class BrowseView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@ -0,0 +1,88 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUIThread
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionEvent
import com.muwire.core.chat.ChatLink
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import net.i2p.data.DataHelper
class ChatConsoleModel {
private final Core core
private final TextGUIThread guiThread
volatile ChatLink link
volatile Thread poller
volatile boolean running
volatile TextBox textBox
ChatConsoleModel(Core core, TextGUIThread guiThread) {
this.core = core
this.guiThread = guiThread
}
void start() {
if (running)
return
running = true
core.chatServer.start()
core.eventBus.with {
register(ChatConnectionEvent.class, this)
publish(new UIConnectChatEvent(host : core.me))
}
}
void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona != core.me)
return // can't really happen
link = e.connection
poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true)
poller.start()
}
void stop() {
if (!running)
return
running = false
core.chatServer.stop()
poller?.interrupt()
link = null
}
private void eventLoop() {
Thread.sleep(1000)
while(running) {
ChatLink link = this.link
if (link == null || !link.isUp()) {
Thread.sleep(100)
continue
}
Object event = link.nextEvent()
if (event instanceof ChatMessageEvent)
handleChatMessage(event)
else if (event instanceof Persona)
handleLeave(event)
else
throw new IllegalArgumentException("unknown event type $event")
}
}
private void handleChatMessage(ChatMessageEvent e) {
String text = DataHelper.formatTime(e.timestamp)+" <"+e.sender.getHumanReadableName()+ "> ["+
e.room+"] "+e.payload
guiThread.invokeLater({textBox.addLine(text)})
}
private void handleLeave(Persona p) {
guiThread.invokeLater({textBox.addLine(p.getHumanReadableName()+ " disconnected")})
}
}

View File

@ -0,0 +1,116 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.Core
import com.muwire.core.chat.ChatCommand
import com.muwire.core.chat.ChatConnection
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import net.i2p.data.DataHelper
class ChatConsoleView extends BasicWindow {
private final TextGUI textGUI
private final ChatConsoleModel model
private final Core core
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
private final LayoutData layoutDataFill = GridLayout.createLayoutData(Alignment.FILL, Alignment.FILL, true, false)
private final TextBox textBox
private final TextBox sayField
private final TextBox roomField
ChatConsoleView(Core core, ChatConsoleModel model, TextGUI textGUI, TerminalSize terminalSize) {
super("Chat Server Console")
this.core = core
this.model = model
this.textGUI = textGUI
TextBox textBox = model.textBox == null ? new TextBox(terminalSize,"", TextBox.Style.MULTI_LINE) : model.textBox
this.textBox = textBox
model.textBox = textBox
model.start()
TerminalSize textFieldSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), 1)
this.sayField = new TextBox(textFieldSize,"", TextBox.Style.SINGLE_LINE)
this.roomField = new TextBox(textFieldSize,"__CONSOLE__", TextBox.Style.SINGLE_LINE)
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
contentPanel.addComponent(textBox, layoutData)
Panel inputPanel = new Panel()
inputPanel.with {
setLayoutManager(new GridLayout(2))
addComponent(new Label("Say something here"), layoutData)
addComponent(sayField, layoutDataFill)
addComponent(new Label("In room:"), layoutData)
addComponent(roomField, layoutDataFill)
}
contentPanel.addComponent(inputPanel, layoutData)
Panel bottomPanel = new Panel()
bottomPanel.setLayoutManager(new GridLayout(5))
Button sayButton = new Button("Say",{say()})
Button startButton = new Button("Start Server",{model.start()})
Button stopButton = new Button("Stop Server", {model.stop()})
Button clearButton = new Button("Clear",{textBox.setText("")})
Button closeButton = new Button("Close",{close()})
bottomPanel.with {
addComponent(sayButton, layoutData)
addComponent(startButton, layoutData)
addComponent(stopButton, layoutData)
addComponent(clearButton, layoutData)
addComponent(closeButton, layoutData)
}
contentPanel.addComponent(bottomPanel, layoutData)
setComponent(contentPanel)
}
private void say() {
String command = sayField.getText()
sayField.setText("")
ChatCommand chatCommand
try {
chatCommand = new ChatCommand(command)
} catch (Exception e) {
chatCommand = new ChatCommand("/SAY $command")
}
command = chatCommand.source
String room = roomField.getText()
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
String toAppend = DataHelper.formatTime(now) + " <" + core.me.getHumanReadableName() + "> [$room] " + command
textBox.addLine(toAppend)
byte[] sig = ChatConnection.sign(uuid, now, room, command, core.me, core.me, core.spk)
def event = new ChatMessageEvent( uuid : uuid,
payload : command,
sender : core.me,
host : core.me,
room : room,
chatTime : now,
sig : sig
)
core.eventBus.publish(event)
}
}

View File

@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.6.5"
private static final String MW_VERSION = "0.6.8"
private static volatile Core core

View File

@ -78,4 +78,32 @@ class FilesModel {
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
}
}
private void sort(SortType type) {
Comparator<SharedFile> chosen
switch(type) {
case SortType.NAME_ASC : chosen = NAME_ASC; break
case SortType.NAME_DESC : chosen = NAME_DESC; break
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
}
Collections.sort(sharedFiles, chosen)
}
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
a.getFile().getName().compareTo(b.getFile().getName())
}
}
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
Long.compare(a.getCachedLength(), b.getCachedLength())
}
}
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@ -51,17 +51,19 @@ class FilesView extends BasicWindow {
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(4))
buttonsPanel.setLayoutManager(new GridLayout(5))
Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button sort = new Button("Sort...",{sort()})
Button close = new Button("Close", {close()})
buttonsPanel.with {
addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData)
addComponent(sort, layoutData)
addComponent(close, layoutData)
}
@ -134,4 +136,11 @@ class FilesView extends BasicWindow {
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
private final UploadsModel uploadsModel
private final FilesModel filesModel
private final TrustModel trustModel
private final ChatConsoleModel chatModel
private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless
@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(textGUI.getGUIThread(), core)
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
if (core.muOptions.startChatServer)
core.chatServer.start()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(7)
GridLayout gridLayout = new GridLayout(8)
buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1))
@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()})
Button chatButton = new Button("Chat", {chat()})
Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData)
addComponent(chatButton, layoutData)
addComponent(quitButton, layoutData)
}
@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
}
private void chat() {
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
}
private void refreshStats() {
int inCon = 0
int outCon = 0

View File

@ -0,0 +1,21 @@
package com.muwire.clilanterna
import com.muwire.core.search.UIResultEvent
class ResultComparators {
public static final Comparator<UIResultEvent> NAME_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
a.name.compareTo(b.name)
}
}
public static final Comparator<UIResultEvent> NAME_DESC = NAME_ASC.reversed()
public static final Comparator<UIResultEvent> SIZE_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
Long.compare(a.size, b.size)
}
}
public static final Comparator<UIResultEvent> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@ -16,7 +16,27 @@ class ResultsModel {
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
results.results.each {
updateModel()
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
Arrays.sort(results.results, chosen)
updateModel()
}
private void updateModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())

View File

@ -43,9 +43,14 @@ class ResultsView extends BasicWindow {
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...",{sort()})
buttonsPanel.addComponent(sortButton)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
buttonsPanel.addComponent(closeButton)
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@ -0,0 +1,57 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
class SortPrompt extends BasicWindow {
private final TextGUI textGUI
private SortType type
SortPrompt(TextGUI textGUI) {
super("Select what to sort by")
this.textGUI = textGUI
}
SortType prompt() {
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(5))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button nameAsc = new Button("Name (ascending)",{
type = SortType.NAME_ASC
close()
})
Button nameDesc = new Button("Name (descending)",{
type = SortType.NAME_DESC
close()
})
Button sizeAsc = new Button("Size (ascending)",{
type = SortType.SIZE_ASC
close()
})
Button sizeDesc = new Button("Size (descending)",{
type = SortType.SIZE_DESC
close()
})
Button close = new Button("Cancel",{close()})
contentPanel.with {
addComponent(nameAsc, layoutData)
addComponent(nameDesc, layoutData)
addComponent(sizeAsc, layoutData)
addComponent(sizeDesc, layoutData)
addComponent(close, layoutData)
}
setComponent(contentPanel)
textGUI.addWindowAndWait(this)
type
}
}

View File

@ -0,0 +1,5 @@
package com.muwire.clilanterna;
public enum SortType {
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
}

View File

@ -2,6 +2,7 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
compile "net.i2p:router:${i2pVersion}"
compile "net.i2p.client:mstreaming:${i2pVersion}"
compile "net.i2p.client:streaming:${i2pVersion}"

View File

@ -91,12 +91,14 @@ public class Core {
final EventBus eventBus
final Persona me
final String version;
final File home
final Properties i2pOptions
final MuWireSettings muOptions
private final TrustService trustService
private final TrustSubscriber trustSubscriber
private final I2PSession i2pSession;
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
private final HostCache hostCache
private final ConnectionManager connectionManager
@ -122,26 +124,27 @@ public class Core {
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
this.version = myVersion
this.muOptions = props
i2pOptions = new Properties()
def i2pOptionsFile = new File(home,"i2p.properties")
// Read defaults
def defaultI2PFile = getClass()
.getClassLoader().getResource("defaults/i2p.properties");
defaultI2PFile.withInputStream { i2pOptions.load(it) }
def i2pOptionsFile = new File(home, "i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
}
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
&& i2pOptions.hasProperty("i2np.udp.port")
)) {
Random r = new Random()
int port = r.nextInt(60000) + 4000
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
@ -150,15 +153,18 @@ public class Core {
}
if (!props.embeddedRouter) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
}
} else {
log.info("launching embedded router")
Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("geoip.dir", home.getAbsolutePath() + File.separator + "geoip")
routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
@ -185,7 +191,6 @@ public class Core {
// options like tunnel length and quantity
I2PSession i2pSession
I2PSocketManager socketManager
keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
@ -274,10 +279,13 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
if (!props.plugin) {
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
} else
log.info("running as plugin, not initializing update client")
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
@ -372,7 +380,7 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
updateClient?.start()
}
public void shutdown() {
@ -382,8 +390,14 @@ public class Core {
}
log.info("saving settings")
saveMuSettings()
log.info("shutting down host cache")
hostCache.stop()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down trust service")
trustService.stop()
log.info("shutting down persister service")
persisterService.stop()
log.info("shutting down download manager")
downloadManager.shutdown()
log.info("shutting down connection acceptor")
@ -400,10 +414,14 @@ public class Core {
chatManager.shutdown()
log.info("shutting down connection manager")
connectionManager.shutdown()
log.info("killing i2p session")
i2pSession.destroySession()
if (router != null) {
log.info("shutting down embedded router")
router.shutdown(0)
}
log.info("shutting down event bus");
eventBus.shutdown()
log.info("shutdown complete")
}
@ -411,6 +429,11 @@ public class Core {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
public void saveI2PSettings() {
File f = new File(home, "i2p.properties")
f.withOutputStream { i2pOptions.store(it, "I2P Options") }
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
@ -436,7 +459,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.5")
Core core = new Core(props, home, "0.6.8")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -2,6 +2,7 @@ package com.muwire.core
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
@ -12,7 +13,7 @@ import groovy.util.logging.Log
class EventBus {
private Map handlers = new HashMap()
private final Executor executor = Executors.newSingleThreadExecutor {r ->
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
def rv = new Thread(r)
rv.setDaemon(true)
rv.setName("event-bus")
@ -53,4 +54,8 @@ class EventBus {
log.info("Unregistering $handler for type $eventType")
handlers[eventType]?.remove(handler)
}
void shutdown() {
executor.shutdownNow()
}
}

View File

@ -34,12 +34,14 @@ class MuWireSettings {
boolean startChatServer
int maxChatConnections
boolean advertiseChat
File chatWelcomeFile
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
boolean plugin
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
@ -75,6 +77,7 @@ class MuWireSettings {
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
@ -85,6 +88,9 @@ class MuWireSettings {
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
String chatWelcomeProp = props.getProperty("chatWelcomeFile")
if (chatWelcomeProp != null)
chatWelcomeFile = new File(chatWelcomeProp)
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
@ -126,6 +132,7 @@ class MuWireSettings {
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("plugin", String.valueOf(plugin))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
@ -136,6 +143,8 @@ class MuWireSettings {
props.setProperty("startChatServer", String.valueOf(startChatServer))
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
if (chatWelcomeFile != null)
props.setProperty("chatWelcomeFile", chatWelcomeFile.getAbsolutePath())
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)

View File

@ -1,20 +1,24 @@
package com.muwire.core.chat;
enum ChatAction {
JOIN(true, false, true),
LEAVE(false, false, true),
SAY(false, false, true),
LIST(true, true, true),
HELP(true, true, true),
INFO(true, true, true),
JOINED(true, true, false);
JOIN(true, false, true, false),
LEAVE(false, false, true, false),
SAY(false, false, true, false),
LIST(true, true, true, false),
HELP(true, true, true, false),
INFO(true, true, true, false),
JOINED(true, true, false, false),
TRUST(true, false, true, true),
DISTRUST(true, false, true, true);
final boolean console;
final boolean stateless;
final boolean user;
ChatAction(boolean console, boolean stateless, boolean user) {
final boolean local;
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
this.console = console;
this.stateless = stateless;
this.user = user;
this.local = local;
}
}

View File

@ -28,10 +28,10 @@ class ChatClient implements Closeable {
private final TrustService trustService
private final MuWireSettings settings
private volatile ChatConnection connection
private volatile boolean connectInProgress
private volatile long lastRejectionTime
private volatile Thread connectThread
private ChatConnection connection
private boolean connectInProgress
private long lastRejectionTime
private Thread connectThread
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
MuWireSettings settings) {
@ -43,15 +43,19 @@ class ChatClient implements Closeable {
this.settings = settings
}
void connectIfNeeded() {
synchronized void connectIfNeeded() {
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
return
connectInProgress = true
CONNECTOR.execute({connect()})
}
private void connect() {
connectInProgress = true
connectThread = Thread.currentThread()
synchronized(this) {
if (!connectInProgress)
return
connectThread = Thread.currentThread()
}
Endpoint endpoint = null
try {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
@ -72,8 +76,11 @@ class ChatClient implements Closeable {
if (code == 429) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
try { dos.close() } catch (IOException ignore) {}
endpoint.close()
lastRejectionTime = System.currentTimeMillis()
synchronized(this) {
lastRejectionTime = System.currentTimeMillis()
}
return
}
@ -88,32 +95,47 @@ class ChatClient implements Closeable {
if (version != Constants.CHAT_VERSION)
throw new Exception("Unknown chat version $version")
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start()
synchronized(this) {
if (!connectInProgress)
return
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start()
}
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
connection : connection))
} catch (Exception e) {
log.log(java.util.logging.Level.WARNING, "connect failed", e)
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
endpoint?.close()
if (endpoint != null) {
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
endpoint.close()
}
} finally {
connectInProgress = false
connectThread = null
synchronized(this) {
connectInProgress = false
connectThread = null
}
}
}
void disconnected() {
synchronized void disconnected() {
connectInProgress = false
connection = null
}
@Override
public void close() {
synchronized public void close() {
connectInProgress = false
connectThread?.interrupt()
connection?.close()
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
}
void ping() {
synchronized void ping() {
connection?.sendPing()
}
synchronized void sendChat(ChatMessageEvent e) {
connection?.sendChat(e)
}
}

View File

@ -109,6 +109,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader", e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}
@ -123,6 +124,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in writer",e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}

View File

@ -7,4 +7,8 @@ class ChatConnectionEvent extends Event {
ChatConnectionAttemptStatus status
Persona persona
ChatLink connection
public String toString() {
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
}
}

View File

@ -51,7 +51,7 @@ class ChatManager {
return
if (e.sender != me)
return
clients[e.host]?.connection?.sendChat(e)
clients[e.host]?.sendChat(e)
}
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {

View File

@ -25,6 +25,8 @@ import net.i2p.util.ConcurrentHashSet
@Log
class ChatServer {
public static final String CONSOLE = "__CONSOLE__"
private static final String DEFAULT_WELCOME = "Welcome to my chat server! Type /HELP for list of available commands"
private final EventBus eventBus
private final MuWireSettings settings
private final TrustService trustService
@ -34,6 +36,7 @@ class ChatServer {
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
private final Map<Persona, Set<String>> memberships = new ConcurrentHashMap<>()
private final Map<String, Persona> shortNames = new ConcurrentHashMap<>()
private final AtomicBoolean running = new AtomicBoolean()
@ -49,10 +52,19 @@ class ChatServer {
}
public void start() {
running.set(true)
if (!running.compareAndSet(false, true))
return
connections.put(me.destination, LocalChatLink.INSTANCE)
joinRoom(me, CONSOLE)
echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination)
shortNames.put(me.getHumanReadableName(), me)
echo(getWelcome(),me.destination)
}
private String getWelcome() {
String welcome = DEFAULT_WELCOME
if (settings.chatWelcomeFile != null)
welcome = settings.chatWelcomeFile.text
"/SAY $welcome"
}
private void sendPings() {
@ -105,8 +117,9 @@ class ChatServer {
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
connections.put(endpoint.destination, connection)
joinRoom(client, CONSOLE)
shortNames.put(client.getHumanReadableName(), client)
connection.start()
echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",connection.endpoint.destination)
echo(getWelcome(),connection.endpoint.destination)
}
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
@ -120,6 +133,7 @@ class ChatServer {
leaveRoom(e.persona, it)
}
}
shortNames.remove(e.persona.getHumanReadableName())
connections.each { k, v ->
v.sendLeave(e.persona)
}
@ -131,7 +145,7 @@ class ChatServer {
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
return
ChatConnection connection = connections.remove(e.persona.destination)
ChatConnection connection = connections.get(e.persona.destination)
connection?.close()
}
@ -187,6 +201,9 @@ class ChatServer {
(!command.action.console && e.room == CONSOLE) ||
!command.action.user)
return
if (command.action.local && e.sender != me)
return
switch(command.action) {
case ChatAction.JOIN : processJoin(command.payload, e); break
@ -195,6 +212,8 @@ class ChatServer {
case ChatAction.LIST : processList(e.sender.destination); break
case ChatAction.INFO : processInfo(e.sender.destination); break
case ChatAction.HELP : processHelp(e.sender.destination); break
case ChatAction.TRUST : processTrust(command.payload, TrustLevel.TRUSTED); break
case ChatAction.DISTRUST : processTrust(command.payload, TrustLevel.DISTRUSTED); break
}
}
@ -264,12 +283,14 @@ class ChatServer {
private void processHelp(Destination d) {
String help = """/SAY
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /HELP
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /TRUST /DISTRUST /HELP
/JOIN <room name> - joins a room, or creates one if it does not exist. You must type this in the console
/LEAVE - leaves a room. You must type this in the room you want to leave
/SAY - optional, says something in the room you're in
/LIST - lists the existing rooms on this server. You must type this in the console
/INFO - shows information about this server. You must type this in the console
/TRUST <user> - marks user as trusted. This is only available to the server owner
/DISTRUST <user> - marks user as distrusted. This is only available to the server owner
/HELP - prints this help message
"""
echo(help, d)
@ -292,6 +313,13 @@ class ChatServer {
connections[d]?.sendChat(echo)
}
private void processTrust(String shortName, TrustLevel level) {
Persona p = shortNames.get(shortName)
if (p == null)
return
eventBus.publish(new TrustEvent(persona : p, level : level))
}
void stop() {
if (running.compareAndSet(true, false)) {
connections.each { k, v ->

View File

@ -369,7 +369,12 @@ class ConnectionAcceptor {
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.running.get() && settings.advertiseChat
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()

View File

@ -92,6 +92,22 @@ public class Downloader {
public synchronized InfoHash getInfoHash() {
infoHash
}
public File getFile() {
file
}
public int getNPieces() {
nPieces
}
public int getPieceSize() {
pieceSize
}
public long getLength() {
length
}
private synchronized void setInfoHash(InfoHash infoHash) {
this.infoHash = infoHash
@ -249,6 +265,10 @@ public class Downloader {
}
active
}
public int getTotalWorkers() {
return activeWorkers.size();
}
public void resume() {
paused = false

View File

@ -10,17 +10,20 @@ import net.i2p.crypto.DSAEngine
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import net.i2p.data.SigningPublicKey
import net.i2p.data.Base64
class Certificate {
private final byte version
private final InfoHash infoHash
private final Name name, comment
private final long timestamp
private final Persona issuer
final Name name, comment
final long timestamp
final Persona issuer
private final byte[] sig
private volatile byte [] payload
private String base64;
Certificate(InputStream is) {
version = (byte) (is.read() & 0xFF)
if (version > Constants.FILE_CERT_VERSION)
@ -131,6 +134,15 @@ class Certificate {
os.write(payload)
}
public String toBase64() {
if (base64 == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
return base64;
}
@Override
public int hashCode() {
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)

View File

@ -32,7 +32,8 @@ class CertificateClient {
fetcherThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING,
user : e.host, infoHash : e.infoHash))
endpoint = connector.connect(e.host.destination)
String infoHashString = Base64.encode(e.infoHash.getRoot())
@ -62,7 +63,8 @@ class CertificateClient {
int count = Integer.parseInt(headers['Count'])
// start pulling the certs
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count,
user : e.host, infoHash : e.infoHash))
DataInputStream dis = new DataInputStream(is)
for (int i = 0; i < count; i++) {
@ -77,11 +79,14 @@ class CertificateClient {
continue
}
if (cert.infoHash == e.infoHash)
eventBus.publish(new CertificateFetchedEvent(certificate : cert))
eventBus.publish(new CertificateFetchedEvent(certificate : cert, user : e.host, infoHash : e.infoHash))
}
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.DONE, count : count,
user : e.host, infoHash : e.infoHash))
} catch (Exception bad) {
log.log(Level.WARNING,"Fetching certificates failed", bad)
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED))
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED,
user : e.host, infoHash : e.infoHash))
} finally {
endpoint?.close()
}

View File

@ -1,8 +1,12 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchEvent extends Event {
CertificateFetchStatus status
int count
Persona user
InfoHash infoHash
}

View File

@ -1,7 +1,11 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchedEvent extends Event {
Certificate certificate
Persona user
InfoHash infoHash
}

View File

@ -119,7 +119,7 @@ class CertificateManager {
added
}
boolean hasLocalCertificate(InfoHash infoHash) {
public boolean hasLocalCertificate(InfoHash infoHash) {
if (!byInfoHash.containsKey(infoHash))
return false
Set<Certificate> set = byInfoHash.get(infoHash)
@ -130,6 +130,13 @@ class CertificateManager {
return false
}
public boolean isImported(Certificate certificate) {
Set<Certificate> forInfoHash = byInfoHash.get(certificate.infoHash)
if (forInfoHash == null)
return false
forInfoHash.contains(certificate)
}
Set<Certificate> getByInfoHash(InfoHash infoHash) {
Set<Certificate> rv = new HashSet<>()
if (byInfoHash.containsKey(infoHash))

View File

@ -0,0 +1,10 @@
package com.muwire.core.files;
import java.io.File;
public interface FileListCallback<T> {
public void onFile(File f, T value);
public void onDirectory(File f);
}

View File

@ -24,7 +24,7 @@ class FileManager {
final Map<String, Set<File>> nameToFiles = new HashMap<>()
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree negativeTree = new FileTree()
final FileTree<Void> negativeTree = new FileTree<>()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
@ -32,7 +32,7 @@ class FileManager {
this.eventBus = eventBus
for (String negative : settings.negativeFileTree) {
negativeTree.add(new File(negative))
negativeTree.add(new File(negative), null)
}
}
@ -88,7 +88,7 @@ class FileManager {
negativeTree.remove(sf.file)
String parent = sf.getFile().getParent()
if (parent != null && settings.watchedDirectories.contains(parent)) {
negativeTree.add(sf.file.getParentFile())
negativeTree.add(sf.file.getParentFile(),null)
}
saveNegativeTree()
@ -128,7 +128,7 @@ class FileManager {
fileToSharedFile.remove(sf.file)
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
negativeTree.add(sf.file)
negativeTree.add(sf.file,null)
saveNegativeTree()
}

View File

@ -2,12 +2,12 @@ package com.muwire.core.files
import java.util.concurrent.ConcurrentHashMap
class FileTree {
class FileTree<T> {
private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
synchronized void add(File file) {
synchronized void add(File file, T value) {
List<File> path = new ArrayList<>()
path.add(file)
while (file.getParentFile() != null) {
@ -29,6 +29,7 @@ class FileTree {
}
current = existing
}
current.value = value;
}
synchronized boolean remove(File file) {
@ -45,13 +46,63 @@ class FileTree {
true
}
public static class TreeNode {
synchronized void traverse(FileTreeCallback<T> callback) {
doTraverse(root, callback);
}
synchronized void traverse(File from, FileTreeCallback<T> callback) {
if (from == null) {
doTraverse(root, callback);
} else {
TreeNode node = fileToNode.get(from);
if (node == null)
return
doTraverse(node, callback);
}
}
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
boolean leave = false
if (node.file != null) {
if (node.file.isFile())
callback.onFile(node.file, node.value)
else {
leave = true
callback.onDirectoryEnter(node.file)
}
}
node.children.each {
doTraverse(it, callback)
}
if (leave)
callback.onDirectoryLeave()
}
synchronized void list(File parent, FileListCallback<T> callback) {
TreeNode<T> node
if (parent == null)
node = root
else
node = fileToNode.get(parent)
node.children.each {
if (it.file.isFile())
callback.onFile(it.file, it.value)
else
callback.onDirectory(it.file)
}
}
public static class TreeNode<T> {
TreeNode parent
File file
T value;
final Set<TreeNode> children = new HashSet<>()
public int hashCode() {
file.hashCode()
Objects.hash(file)
}
public boolean equals(Object o) {

View File

@ -0,0 +1,9 @@
package com.muwire.core.files;
import java.io.File;
public interface FileTreeCallback<T> {
public void onDirectoryEnter(File file);
public void onDirectoryLeave();
public void onFile(File file, T value);
}

View File

@ -1,5 +1,7 @@
package com.muwire.core.hostcache
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionManager
@ -27,6 +29,7 @@ class CacheClient {
final long interval
final MuWireSettings settings
final Timer timer
private final AtomicBoolean stopped = new AtomicBoolean();
public CacheClient(EventBus eventBus, HostCache cache,
ConnectionManager manager, I2PSession session,
@ -47,9 +50,12 @@ class CacheClient {
void stop() {
timer.cancel()
stopped.set(true)
}
private void queryIfNeeded() {
if (stopped.get())
return
if (!manager.getConnections().isEmpty())
return
if (!cache.getHosts(1).isEmpty())
@ -65,7 +71,12 @@ class CacheClient {
options.setSendLeaseSet(true)
CacheServers.getCacheServers().each {
log.info "Querying hostcache ${it.toBase32()}"
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
try {
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
} catch (Exception e) {
if (!stopped.get())
throw e
}
}
}

View File

@ -35,7 +35,7 @@ class BrowseManager {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
eventBus.publish(new BrowseStatusEvent(host : e.host, status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
@ -55,8 +55,10 @@ class BrowseManager {
int results = Integer.parseInt(headers['Count'])
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
// at this stage, start pulling the results
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING, totalResults : results))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FETCHING, totalResults : results))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
@ -67,14 +69,15 @@ class BrowseManager {
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
result.chat = chat
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}

View File

@ -1,8 +1,10 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
class BrowseStatusEvent extends Event {
Persona host
BrowseStatus status
int totalResults
}

View File

@ -10,8 +10,8 @@ import net.i2p.util.ConcurrentHashSet
class RemoteTrustList {
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
private final Persona persona
private final Set<TrustEntry> good, bad
final Persona persona
final Set<TrustEntry> good, bad
volatile long timestamp
volatile boolean forceUpdate
Status status = Status.NEW

View File

@ -130,8 +130,8 @@ class TrustService extends Service {
}
public static class TrustEntry {
private final Persona persona
private final String reason
final Persona persona
final String reason
TrustEntry(Persona persona, String reason) {
this.persona = persona
this.reason = reason

View File

@ -26,7 +26,7 @@ class TrustSubscriber {
private final I2PConnector i2pConnector
private final MuWireSettings settings
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
private final Object waitLock = new Object()
private volatile boolean shutdown
@ -50,7 +50,7 @@ class TrustSubscriber {
thread?.interrupt()
updateThreads.shutdownNow()
}
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
if (!e.subscribe) {
remoteTrustLists.remove(e.persona.destination)
@ -62,6 +62,10 @@ class TrustSubscriber {
}
}
}
public boolean isSubscribed(Persona p) {
remoteTrustLists.containsKey(p.destination)
}
private void checkLoop() {
try {

View File

@ -11,6 +11,7 @@ public class Constants {
public static final int MAX_HEADER_SIZE = 0x1 << 14;
public static final int MAX_HEADERS = 16;
public static final long MAX_HEADER_TIME = 60 * 1000;
public static final int MAX_RESULTS = 0x1 << 16;

View File

@ -68,11 +68,19 @@ public class Persona {
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32);
return humanReadableName;
}
public Destination getDestination() {
return destination;
}
public String toBase64() throws DataFormatException, IOException {
public String toBase64() {
if (base64 == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
write(baos);
try {
write(baos);
} catch (Exception impossible) {
throw new RuntimeException(impossible);
}
base64 = Base64.encode(baos.toByteArray());
}
return base64;

View File

@ -63,7 +63,7 @@ public class DataUtil {
((int)header[2] & 0xFF);
}
static String readi18nString(byte [] encoded) {
public static String readi18nString(byte [] encoded) {
if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
@ -91,9 +91,12 @@ public class DataUtil {
}
public static String readTillRN(InputStream is) throws IOException {
final long start = System.currentTimeMillis();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
int read = is.read();
if (System.currentTimeMillis() - start > Constants.MAX_HEADER_TIME)
throw new IOException("header taking too long");
if (read == -1)
throw new IOException();
if (read == '\r') {

View File

@ -0,0 +1,8 @@
inbound.nickname=MuWire
outbound.nickname=MuWire
inbound.length=3
inbound.quantity=4
outbound.length=3
outbound.quantity=4
i2cp.tcp.host=127.0.0.1
i2cp.tcp.port=7654

View File

@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null, null)
acceptor.start()
Thread.sleep(100)
}

View File

@ -10,8 +10,8 @@ class FileTreeTest {
File b = new File(a, "b")
File c = new File(b, "c")
FileTree tree = new FileTree()
tree.add(c)
FileTree<Void> tree = new FileTree<>()
tree.add(c,null)
assert tree.root.children.size() == 1
assert tree.fileToNode.size() == 3
@ -28,15 +28,110 @@ class FileTreeTest {
File c = new File(b, "c")
File d = new File(b, "d")
FileTree tree = new FileTree()
tree.add(c)
FileTree<Void> tree = new FileTree<>()
tree.add(c,null)
assert tree.fileToNode.size() == 3
tree.add(d)
tree.add(d, null)
assert tree.fileToNode.size() == 4
tree.remove(d)
assert tree.fileToNode.size() == 3
}
@Test
public void testTraverse() {
Stack stack = new Stack()
Set<String> values = new HashSet<>()
StringBuilder sb = new StringBuilder()
def cb = new FileTreeCallback<String>() {
@Override
public void onDirectoryEnter(File file) {
stack.push(file)
}
@Override
public void onDirectoryLeave() {
stack.pop()
}
@Override
public void onFile(File file, String value) {
values.add(value)
}
}
File a = new File("a")
a.createNewFile()
File b = new File("b")
b.mkdir()
File c = new File(b, "c")
c.createNewFile()
File d = new File(b, "d")
d.mkdir()
File e = new File(d, "e")
e.createNewFile()
FileTree<String> tree = new FileTree<>()
tree.add(a, "a")
tree.add(b, "b")
tree.add(c, "c")
tree.add(d, "d")
tree.add(e, "e")
tree.traverse(cb)
assert stack.isEmpty()
assert values.size() == 3
assert values.contains("a")
assert values.contains("c")
assert values.contains("e")
}
@Test
public void testList() {
Set<File> directories = new HashSet<>()
Set<String> values = new HashSet<>()
def cb = new FileListCallback<String>() {
@Override
public void onDirectory(File file) {
directories.add(file)
}
@Override
public void onFile(File file, String value) {
values.add(value)
}
}
File a = new File("a")
a.createNewFile()
File b = new File("b")
b.mkdir()
File c = new File(b, "c")
c.createNewFile()
FileTree<String> tree = new FileTree<>()
tree.add(a, "a")
tree.add(b, "b")
tree.add(c, "c")
tree.list(null, cb)
assert directories.size() == 1
assert directories.contains(b)
assert values.size() == 1
assert values.contains("a")
directories.clear()
values.clear()
tree.list(b, cb)
assert directories.isEmpty()
assert values.size() == 1
assert values.contains("c")
}
}

59
doc/architecture.md Normal file
View File

@ -0,0 +1,59 @@
# MuWire architecture
### Core-UI separation
The MuWire application is split conceptually into a `core` component and two `ui` components - one graphical component which is build using Swing and one text-only component built using the "lanterna" library.
The core is written in mixture of Java and Groovy and is designed to be easy to embed into any application or language running on a JVM. To achieve this, all communicatioon between the core and the outside world happens over an event bus using event objects.
### Event bus and events
At the heart of the core is the event bus. It allows the different components that comprise the core to be decoupled, and allows the external components like UIs to communicate in asynchronous fashion with the core.
The Core object has a single instance of the `com.muwire.core.EventBus` class. It is responsible for dispatching events to any registered listeners. Events themselves extend the `com.muwire.core.Event` class and carry arbitrary information relevant to the event. See below or an example how to build a custom event:
1. Define the event in a class that extends `com.muwire.core.Event`:
```
package mypackage
import com.muwire.core.Event
class MyEvent extends Event {
// add relevant fields here
}
```
2. Define one or more classes that will be notified of your events:
```
package mypackage
class MyEventListener {
// ... add other logic here
void onMyEvent(MyEvent e) {
// logic to handle your type of event
}
}
```
3. Register your event listener with the event bus:
```
MyEventListener myListener = new MyEventListener()
eventBus.register(MyEvent.class,myListener)
```
You can register more than one listener for the same type of event; they will be notified in the order you register them.
4. Publish events to the event bus
```
MyEvent myEvent = new MyEvent()
// ... set relevant fields of the event ...
eventBus.publish(myEvent)
```
Threading: the event bus creates a dedicated thread and all events are dispatched on that thread, regardless which thread publishes them.
### Sharing files
The UI publishes an event of type `com.muwire.core.files.FileSharedEvent` which contains a `java.io.File` reference to the file the user has chosen to share. A component in the core called `HasherService` listens for these events, and when it receives notification that a FileSharedEvent has been posted it pereforms some sanity checks, then offloads the actual hashing to a dedicated thread.
Before the hashing begins, another event of type `com.muwire.core.files.FileHashingEvent` is published that contains the name of the file. At the moment that event serves only to update the UI with the current file being hashed.
When the hashing completes, a `com.muwire.core.files.FileHashedEvent` is published by the HasherService. The UI listens to this event and updates its list of shared files. Another core component called `FileManager` also listens for such events and updates the interenal search index from the file name.

98
doc/chat.md Normal file
View File

@ -0,0 +1,98 @@
# MuWire Chat System
Since version 0.6.3 MuWire comes with a built in chat system. It is very similar to the way IRC operates, and the user experience mimics that of IRC as well.
### Design
The chat system uses a client-server model. Each MuWire node can run a chat server which accepts incoming connections; clients wishing to connect to a chat server establish an outgoing streaming connection to the destination where the chat server is running. The local client also connects to server through a special "loopback" connection.
Once connected, the client automatically joins a special room called "__CONSOLE__" which is the server console. In that room users can issue certain commands, but cannot actually chat. In order to chat, the client needs to `/join` a chat room first. The chat room is kept as state in the server and any messages sent to that chat room are forwarded to all other users who have joined the same room. When the last member of a room leaves, the room state is destroyed server-side.
Private messages work by replacing the room of the message with the base64-encoded persona of the recipient of the message.
### Chat Commands
Clients issue commands to the chat server in order to perform operations. Some commands can only be issued in the __CONSOLE__ room, others only in a regular chat room. The server will ignore commands which are not issued in the appropriate place.
There are several chat commands that MuWire supports, more can be added later. Commands consist of a prefix and payload. The prefix always beings with forward slash `/`. Below is the list of commands a MuWire chat server supports as of version 0.6.6:
##### /HELP - this command can be issued only in the __CONSOLE__ room. It results in the server echoing back a help message of the commands it supports.
##### /SAY - this command can be issued only in a regular chat room or private chat. It's payload is the content of what the user wishes to say.
##### /INFO - this command can be issued only in the __CONSOLE__ room. It results in the server printing a status message. As of 0.6.6, this consists of the base64-encoded address of the server as well as a list of user who are currently connected.
##### /LIST - this command can be issued only in the __CONSOLE__ room. It results in the server echoing the list of rooms which currently have at least one member.
##### /JOIN - this command can be issued only in the __CONSOLE_ room. The payload of the command is the name of the room that the user wishes to join. This results in server-side state being updated to add the user to the membership list of the room.
##### /LEAVE - this command can be issued only in a regular room. It has no payload, and the result is that the server removes the user issuing the command from the room.
##### /TRUST - this command can be issued only in the __CONSOLE__ room and only over the loopback connection, i.e. it is reserved for the owner of the server. It's payload is the human-readable representation of a user the owner wishes to mark as trusted. It results in adding the specified user to the owner's trust list.
##### /DISTRUST - similar to /TRUST, this command results in the opposite; the user specified in the payload being added to the distrusted list. This also results in the user getting disconnected from the server, i.e. kick/ban-ned.
There is a command called "/JOINED" which is issued from the server to the client upon the client joining a room. The payload of the command is a comma-separated list of base64-encoded representations of the personas of the users already in that room.
### Protocol
The client wishing to connect to a server establishes an I2P connection and sends the letters "IRC\r\n" in ASCII encoding. These are followed by one more headers, each header consisting of a name, followed by colon, followed by value, terminated with "\r\n". After all headers have been sent, an additional "\r\n" is written to the socket.
As of version 0.6.6 the following headers are required:
* "Version" - this header indicates the version of the chat protocol that will be used over this connection. Currently fixed at 1.
* "Persona" - this header contains the base64-encoded representation of the persona of the client.
The server responds with a status code encoded as an aSCII string, terminated with "\r\n", which can be one of the following:
* 200 - connection accepted
* 400 - connection not allowed. This can be issued if the server is down for example.
* 429 - connection rejected. This can be issued when the server is overloaded or the client is already connected to the server. Clients are encouraged to not re-attempt connecting for a short period of time.
After the code, the server responds with a "Version" header followed by a "\r\n" on an empty line.
### Messages
After the headers have been exchanged, the connection starts transmitting messages back and forth. Messages are encoded in UTF-8 JSON format, and preceeded by two bytes which are the unsigned representation of the number of bytes of JSON.
As of protocol version 1, the following messages are supported:
##### "Keepalive Ping".
This message serves only to prevent the blocking read from I2P sockets from timing out and is sent on regular intervals by both the server and the client. Example payload of such message is:
```
{
"type" : "Ping",
"version" : 1
}
```
##### "Chat Command"
This message is sent by both server and client whenever an event occurs, such as user issuing a command, or another user in a room the user has joined issues a command. The payload is the following:
```
{
"type" : "Chat",
"uuid" : "1234-asdf-...", // unique random UUID of this message
"host" : "asdf123..", // base64-encoded persona of the server owner, i.e. the server this message is destined to
"sender" : "asdf123...", // base64-encoded persona of the sender of the message. The server verifies it matches the destination of the I2P socket it was received from.
"chatTime" : 1235..., // time since epoch in milliseconds when the message was sent.
"room" : "asdf..." // UTF-8 string indicating the room this message is destined to
"payload" : "/SAY asdf..." // UTF-8 string of the chat command being issued by the user.
"sig" : "asdf1234..." // base64-encoded signature.
}
```
In order to prevent spoofing and replay attacks, each Chat Command message contains a signature. The signature covers the following fields in this order:
1. uuid - toString() representation of the UUID
2. host - binary representation of the persona in the host field
3. sender - binary representation of the persona in the sender field
4. chatTime - big endian representation of the timestamp of the message (8 bytes)
5. room - UTF-8 representation of the room field
6. payload - UTF-8 representation of the payload field.
The signature is created with the signing private key (SPK) of the sender.
##### "Leave"
This message is only sent from a server to a client, whenever another client disconnects from the server. It's format is the following:
```
{
"type" : "Leave,
"persona" : "asdf1234..." // base64-encoded persona of the user being disconnected from the server.
}
```
### Future Work
It is possible to extend this protocol to support inter-server relaying of messages. Because every Chat Command message is signed, it will not be possible for malicious server operators to spoof its contents.

View File

@ -0,0 +1,26 @@
#!/usr/bin/with-contenv sh
#
# Add the app user to the password and group databases. This is needed just to
# make sure that mapping between the user/group ID and its name is possible.
#
set -e # Exit immediately if a command exits with a non-zero status.
set -u # Treat unset variables as an error.
cp /defaults/passwd /etc/passwd
cp /defaults/group /etc/group
cp /defaults/shadow /etc/shadow
chown root:shadow /etc/shadow
chmod 640 /etc/shadow
echo "$APP_USER:x:$USER_ID:$GROUP_ID::${APP_HOME:-/dev/null}:/sbin/nologin" >> /etc/passwd
echo "$APP_USER:x:$GROUP_ID:" >> /etc/group
# Make sure APP_HOME is editable by the user
if [[ -n "$APP_HOME" ]] ; then
chown -R "$APP_USER" "$APP_HOME"
chmod -R u+rw "$APP_HOME"
fi
# vim:ft=sh:ts=4:sw=4:et:sts=4

View File

@ -0,0 +1,34 @@
#This file is UTF-8
#Tue Jan 14 12:08:47 GMT 2020
meshExpiration=60
autoDownloadUpdate=true
hostHopelessInterval=1440
uploadSlotsPerUser=-1
downloadLocation=/output
allowTrustLists=true
embeddedRouter=false
incompleteLocation=/incompletes
outBw=128
searchExtraHop=false
shareHiddenFiles=false
advertiseChat=true
totalUploadSlots=-1
hostClearInterval=15
searchComments=true
downloadSequentialRatio=0.8
maxChatConnectios=-1
trustListInterval=1
crawlerResponse=REGISTERED
browseFiles=true
lastUpdateCheck=1579003533112
hostRejectInterval=1
inBw=256
leaf=false
updateCheckInterval=24
plugin=false
downloadRetryInterval=60
speedSmoothSeconds=60
allowUntrusted=true
shareDownloadedFiles=true
startChatServer=false
updateType=jar

View File

@ -0,0 +1 @@
i2cp.tcp.host=172.17.0.1

View File

@ -0,0 +1,7 @@
#!/bin/sh
# Explicitly define HOME otherwise it might not have been set
export HOME=/muwire
echo "Starting MuWire"
exec /muwire/bin/MuWire

View File

@ -1,6 +1,6 @@
group = com.muwire
version = 0.6.5
i2pVersion = 0.9.43
version = 0.6.8
i2pVersion = 0.9.44
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
@ -16,6 +16,6 @@ author = zab@mail.i2p
signer = zab@mail.i2p
keystorePassword=changeit
websiteURL=http://muwire.i2p
updateURLsu3=http://muwire.i2p/MuWire.su3
updateURLsu3=http://muwire.i2p/MuWire-update.su3
pack200=true

View File

@ -121,4 +121,9 @@ mvcGroups {
view = 'com.muwire.gui.ChatRoomView'
controller = 'com.muwire.gui.ChatRoomController'
}
'chat-monitor' {
model = 'com.muwire.gui.ChatMonitorModel'
view = 'com.muwire.gui.ChatMonitorView'
controller = 'com.muwire.gui.ChatMonitorController'
}
}

View File

@ -47,6 +47,7 @@ class BrowseController {
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.chatActionEnabled = e.chat
model.results << e
model.resultCount = model.results.size()
view.resultsTable.model.fireTableDataChanged()
@ -64,8 +65,11 @@ class BrowseController {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
def group = application.mvcGroupManager.getGroups()['MainFrame']
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
!group.model.canDownload(it.infohash)
}
selectedResults.each { result ->
@ -74,11 +78,11 @@ class BrowseController {
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
sequential : view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
group.view.showDownloadsWindow.call()
dismiss()
}
@ -113,4 +117,13 @@ class BrowseController {
params['core'] = core
mvcGroup.createMVCGroup("fetch-certificates", params)
}
@ControllerAction
void chat() {
dismiss()
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
mainFrameGroup.controller.startChat(model.host)
mainFrameGroup.view.showChatWindow.call()
}
}

View File

@ -0,0 +1,13 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class ChatMonitorController {
@MVCMember @Nonnull
ChatMonitorModel model
}

View File

@ -63,7 +63,8 @@ class ChatRoomController {
if (command.action == ChatAction.JOIN) {
String newRoom = command.payload
if (!mvcGroup.parentGroup.childrenGroups.containsKey(newRoom)) {
String groupId = model.host.getHumanReadableName()+"-"+newRoom
if (!mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
def params = [:]
params['core'] = model.core
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
@ -71,8 +72,9 @@ class ChatRoomController {
params['console'] = false
params['host'] = model.host
params['roomTabName'] = newRoom
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, params)
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
}
}
if (command.action == ChatAction.LEAVE && !model.console) {
@ -110,6 +112,7 @@ class ChatRoomController {
params['privateChat'] = true
params['host'] = model.host
params['roomTabName'] = p.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
}
@ -153,7 +156,7 @@ class ChatRoomController {
}
void leaveRoom() {
if (leftRoom)
if (leftRoom || model.privateChat)
return
leftRoom = true
long now = System.currentTimeMillis()
@ -191,6 +194,8 @@ class ChatRoomController {
runInsideUIAsync {
view.roomTextArea.append(toDisplay)
trimLines()
if (!model.console)
view.chatNotificator.onMessage(mvcGroup.mvcId)
}
}
@ -246,7 +251,7 @@ class ChatRoomController {
}
void rejoinRoom() {
if (model.room == "Console")
if (model.console || model.privateChat)
return
model.members.clear()
@ -267,4 +272,11 @@ class ChatRoomController {
)
model.core.eventBus.publish(event)
}
void serverDisconnected() {
runInsideUIAsync {
model.members.clear()
view.membersTable?.model?.fireTableDataChanged()
}
}
}

View File

@ -18,6 +18,9 @@ class ChatServerController {
switch(model.buttonText) {
case "Disconnect" :
model.buttonText = "Connect"
mvcGroup.getChildrenGroups().each { k,v ->
v.controller.serverDisconnected()
}
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
break
case "Connect" :

View File

@ -23,6 +23,8 @@ class I2PStatusController {
Router router = core.router
model.networkStatus = router._context.commSystem().status.toStatusString()
model.floodfill = router._context.netDb().floodfillEnabled()
model.myCountry = router._context.commSystem().getOurCountry()
model.strictCountry = router._context.commSystem().isInStrictCountry()
model.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()

View File

@ -14,6 +14,8 @@ import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
@ -87,8 +89,19 @@ class MainFrameController {
search = search.trim()
if (search.length() == 0)
return
if (search.length() > 128)
search = search.substring(0,128)
if (search.length() > 128) {
try {
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(search)))
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
return
} catch (Exception notPersona) {
search = search.substring(0,128)
}
}
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
@ -458,6 +471,7 @@ class MainFrameController {
def params = [:]
params['core'] = model.core
params['host'] = model.core.me
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
}
}
@ -484,11 +498,28 @@ class MainFrameController {
startChat(p)
}
@ControllerAction
void copyShort() {
copy(model.core.me.getHumanReadableName())
}
@ControllerAction
void copyFull() {
copy(model.core.me.toBase64())
}
private void copy(String s) {
StringSelection selection = new StringSelection(s)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
void startChat(Persona p) {
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
def params = [:]
params['core'] = model.core
params['host'] = p
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
} else
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()

View File

@ -155,6 +155,9 @@ class OptionsController {
int maxChatLines = Integer.parseInt(view.maxChatLinesField.text)
model.maxChatLines = maxChatLines
uiSettings.maxChatLines = maxChatLines
if (model.chatWelcomeFile != null)
settings.chatWelcomeFile = new File(model.chatWelcomeFile)
core.saveMuSettings()
@ -237,6 +240,19 @@ class OptionsController {
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void chooseChatFile() {
def chooser = new JFileChooser()
chooser.with {
setFileHidingEnabled(false)
setDialogTitle("Select location of chat server welcome file")
setFileSelectionMode(JFileChooser.FILES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.chatWelcomeFile = getSelectedFile().getAbsolutePath()
}
}
@ControllerAction
void automaticFont() {
model.automaticFontSize = true

View File

@ -44,6 +44,8 @@ class Ready extends AbstractLifecycleHandler {
propsFile.withReader("UTF-8", {
props.load(it)
})
if (!props.containsKey("nickname"))
props.setProperty("nickname", selectNickname())
props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
@ -53,25 +55,7 @@ class Ready extends AbstractLifecycleHandler {
props.incompleteLocation = new File(home, "incompletes")
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
props.updateType = System.getProperty("updateType","jar")
def nickname
while (true) {
nickname = JOptionPane.showInputDialog(null,
"Your nickname is displayed when you send search results so other MuWire users can choose to trust you",
"Please choose a nickname", JOptionPane.PLAIN_MESSAGE)
if (nickname == null || nickname.trim().length() == 0) {
JOptionPane.showMessageDialog(null, "Nickname cannot be empty", "Select another nickname",
JOptionPane.WARNING_MESSAGE)
continue
}
if (nickname.contains("@")) {
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
"Select another nickname", JOptionPane.WARNING_MESSAGE)
continue
}
nickname = nickname.trim()
break
}
props.setNickname(nickname)
props.setNickname(selectNickname())
def portableDownloads = System.getProperty("portable.downloads")
@ -116,5 +100,31 @@ class Ready extends AbstractLifecycleHandler {
core.eventBus.publish(new UILoadedEvent())
}
private String selectNickname() {
String nickname
while (true) {
nickname = JOptionPane.showInputDialog(null,
"Your nickname is displayed when you send search results so other MuWire users can choose to trust you",
"Please choose a nickname", JOptionPane.PLAIN_MESSAGE)
if (nickname == null) {
JOptionPane.showMessageDialog(null, "MuWire cannot start without a nickname and will now exit", JOptionPane.PLAIN_MESSAGE)
System.exit(0)
}
if (nickname.trim().length() == 0) {
JOptionPane.showMessageDialog(null, "Nickname cannot be empty", "Select another nickname",
JOptionPane.WARNING_MESSAGE)
continue
}
if (nickname.contains("@")) {
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
"Select another nickname", JOptionPane.WARNING_MESSAGE)
continue
}
nickname = nickname.trim()
break
}
nickname
}
}

View File

@ -15,6 +15,7 @@ class BrowseModel {
@Observable boolean downloadActionEnabled
@Observable boolean viewCommentActionEnabled
@Observable boolean viewCertificatesActionEnabled
@Observable boolean chatActionEnabled
@Observable int totalResults
@Observable int resultCount

View File

@ -0,0 +1,46 @@
package com.muwire.gui
import javax.annotation.Nonnull
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class ChatMonitorModel implements ChatNotificator.Listener {
@MVCMember @Nonnull
ChatMonitorView view
ChatNotificator chatNotificator
def rooms = []
void mvcGroupInit(Map<String,String> args) {
chatNotificator.listener = this
}
void mvcGroupDestroy() {
chatNotificator.listener = null
}
public void update() {
rooms.clear()
chatNotificator.roomsWithMessages.each { room, count ->
int dash = room.indexOf('-')
String server = room.substring(0, dash)
String roomName = room.substring(dash + 1)
rooms.add(new ChatRoomEntry(server, roomName, count))
}
view.updateView()
}
private static class ChatRoomEntry {
private final String server, room
private final int count
ChatRoomEntry(String server, String room, int count) {
this.server = server
this.room = room
this.count = count
}
}
}

View File

@ -2,6 +2,8 @@ package com.muwire.gui
import java.util.logging.Level
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand
@ -13,6 +15,7 @@ import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import groovy.util.logging.Log
import griffon.metadata.ArtifactProviderFor
@ -20,12 +23,16 @@ import griffon.metadata.ArtifactProviderFor
@Log
@ArtifactProviderFor(GriffonModel)
class ChatServerModel {
@MVCMember @Nonnull
ChatServerView view
Persona host
Core core
@Observable boolean disconnectActionEnabled
@Observable String buttonText = "Disconnect"
@Observable ChatConnectionAttemptStatus status
@Observable boolean sayActionEnabled
volatile ChatLink link
volatile Thread poller
@ -71,6 +78,7 @@ class ChatServerModel {
runInsideUIAsync {
status = e.status
sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
}
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
@ -131,6 +139,7 @@ class ChatServerModel {
params['privateChat'] = true
params['host'] = host
params['roomTabName'] = e.sender.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-room",groupId, params)
}

View File

@ -16,6 +16,8 @@ class I2PStatusModel {
@Observable int ssuConnections
@Observable String networkStatus
@Observable boolean floodfill
@Observable String myCountry
@Observable boolean strictCountry
@Observable int participatingTunnels
@Observable int activePeers
@Observable int receiveBps

View File

@ -106,6 +106,8 @@ class MainFrameModel {
@Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled
@Observable boolean browseFromTrustedButtonEnabled
@Observable boolean chatFromTrustedButtonEnabled
@Observable boolean markNeutralFromDistrustedButtonEnabled
@Observable boolean markTrustedButtonEnabled
@Observable boolean reviewButtonEnabled

View File

@ -61,6 +61,7 @@ class OptionsModel {
@Observable int maxChatConnections
@Observable boolean advertiseChat
@Observable int maxChatLines
@Observable String chatWelcomeFile
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
@ -114,5 +115,6 @@ class OptionsModel {
maxChatConnections = settings.maxChatConnections
advertiseChat = settings.advertiseChat
maxChatLines = uiSettings.maxChatLines
chatWelcomeFile = settings.chatWelcomeFile?.getAbsolutePath()
}
}

View File

@ -39,6 +39,8 @@ class BrowseView {
def p
def resultsTable
def lastSortEvent
def sequentialDownloadCheckbox
void initUI() {
int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame")
@ -66,7 +68,10 @@ class BrowseView {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
button(text : "Chat", enabled : bind {model.chatActionEnabled}, chatAction)
button(text : "Dismiss", dismissAction)
label(text : "Download sequentially")
sequentialDownloadCheckbox = checkBox()
}
}
@ -106,8 +111,9 @@ class BrowseView {
else
model.viewCommentActionEnabled = false
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
downloadActionEnabled &= mainFrameGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled

View File

@ -0,0 +1,62 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JFrame
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class ChatMonitorView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
ChatMonitorModel model
def window
def roomsTable
void initUI() {
int rowHeight = application.context.getAsInt("row-height")
window = builder.frame (visible : false, locationRelativeTo : null,
defaultCloseOperation : JFrame.DISPOSE_ON_CLOSE,
iconImage : builder.imageIcon("/MuWire-48x48.png").image){
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label("Chat rooms with unread messages")
}
scrollPane(constraints : BorderLayout.CENTER) {
roomsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.rooms) {
closureColumn(header : "Server", type: String, read : {it.server})
closureColumn(header : "Room", type : String, read : {it.room})
closureColumn(header : "Messages", type : Integer, read : {it.count})
}
}
}
}
}
void updateView() {
roomsTable.model.fireTableDataChanged()
}
void mvcGroupInit(Map<String,String> args) {
window.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
window.pack()
window.setVisible(true)
}
}

View File

@ -12,6 +12,7 @@ import javax.swing.SwingConstants
import javax.swing.SpringLayout.Constraints
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionAttemptStatus
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
@ -28,29 +29,33 @@ class ChatRoomView {
@MVCMember @Nonnull
ChatRoomController controller
ChatNotificator chatNotificator
def pane
def parent
def sayField
def roomTextArea
def textScrollPane
def membersTable
def lastMembersTableSortEvent
void initUI() {
int rowHeight = application.context.get("row-height")
def parentModel = mvcGroup.parentGroup.model
if (model.console || model.privateChat) {
pane = builder.panel {
borderLayout()
panel(constraints : BorderLayout.CENTER) {
gridLayout(rows : 1, cols : 1)
scrollPane {
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel(constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(enabled : bind {parentModel.sayActionEnabled},text : "Say", constraints : BorderLayout.EAST, sayAction)
}
}
} else {
@ -72,7 +77,7 @@ class ChatRoomView {
}
panel {
gridLayout(rows : 1, cols : 1)
scrollPane {
textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
}
}
@ -81,12 +86,15 @@ class ChatRoomView {
panel(constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(enabled : bind {parentModel.sayActionEnabled}, text : "Say", constraints : BorderLayout.EAST, sayAction)
}
}
}
SmartScroller smartScroller = new SmartScroller(textScrollPane)
pane.putClientProperty("mvcId", mvcGroup.mvcId)
}
void mvcGroupInit(Map<String,String> args) {
@ -168,6 +176,7 @@ class ChatRoomView {
int index = parent.indexOfComponent(pane)
parent.removeTabAt(index)
controller.leaveRoom()
chatNotificator.roomClosed(mvcGroup.mvcId)
mvcGroup.destroy()
}
}

View File

@ -19,14 +19,17 @@ class ChatServerView {
ChatServerModel model
@MVCMember @Nonnull
ChatServerController controller
ChatNotificator chatNotificator
def pane
def parent
def childPane
void initUI() {
pane = builder.panel {
borderLayout()
tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
childPane = tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows : 1, cols : 3)
panel {}
@ -39,6 +42,9 @@ class ChatServerView {
}
}
}
pane.putClientProperty("mvcId",mvcGroup.mvcId)
pane.putClientProperty("childPane", childPane)
childPane.addChangeListener({e -> chatNotificator.roomTabChanged(e.getSource())})
}
void mvcGroupInit(Map<String,String> args) {
@ -69,11 +75,16 @@ class ChatServerView {
params['roomTabName'] = 'Console'
params['console'] = true
params['host'] = model.host
params['chatNotificator'] = chatNotificator
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
}
def closeTab = {
controller.disconnect()
if (model.host == model.core.me) {
mvcGroup.parentGroup.controller.stopChatServer()
}
else if (model.buttonText == "Disconnect")
controller.disconnect()
int index = parent.indexOfComponent(pane)
parent.removeTabAt(index)
mvcGroup.destroy()

View File

@ -45,6 +45,10 @@ class I2PStatusView {
label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:2, anchor : GridBagConstraints.LINE_END))
label(text : "Our Country", constraints : gbc(gridx: 0, gridy: 3, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.myCountry}, constraints : gbc(gridx : 1, gridy: 3, anchor : GridBagConstraints.LINE_END))
label(text : "Strict Country", constraints : gbc(gridx:0, gridy:4, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.strictCountry}, constraints : gbc(gridx : 1, gridy : 4, anchor : GridBagConstraints.LINE_END))
}
panel(border : titledBorder(title : "Connections", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {

View File

@ -78,8 +78,10 @@ class MainFrameView {
UISettings settings
ChatNotificator chatNotificator
void initUI() {
chatNotificator = new ChatNotificator(application.getMvcGroupManager())
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
@ -129,6 +131,13 @@ class MainFrameView {
env['core'] = model.core
mvcGroup.createMVCGroup("certificate-control",env)
})
menuItem("Chat Room Monitor", actionPerformed : {
if (!mvcGroup.getChildrenGroups().containsKey("chat-monitor")) {
def env = [:]
env['chatNotificator'] = chatNotificator
mvcGroup.createMVCGroup("chat-monitor","chat-monitor",env)
}
})
}
}
borderLayout()
@ -434,8 +443,8 @@ class MainFrameView {
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
button(text : "Mark Neutral", enabled : bind {model.markNeutralFromTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
button(text : "Browse", constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
button(text : "Chat", constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
button(text : "Browse", enabled : bind{model.browseFromTrustedButtonEnabled}, constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
button(text : "Chat", enabled : bind{model.chatFromTrustedButtonEnabled} ,constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
}
}
panel (border : etchedBorder()){
@ -490,7 +499,11 @@ class MainFrameView {
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
label(text : bind {model.me}, constraints: BorderLayout.CENTER)
panel (constraints : BorderLayout.WEST) {
label(text : bind {model.me})
button(text : "Copy Short", copyShortAction)
button(text : "Copy Full", copyFullAction)
}
panel (constraints : BorderLayout.EAST) {
label("Connections:")
label(text : bind {model.connections})
@ -522,6 +535,7 @@ class MainFrameView {
mainFrame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
if (application.getContext().get("tray-icon")) {
if (settings.closeWarning) {
runInsideUIAsync {
@ -535,6 +549,13 @@ class MainFrameView {
} else {
closeApplication()
}
}
public void windowDeactivated(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
}
public void windowActivated(WindowEvent e) {
if (!model.chatPaneButtonEnabled)
chatNotificator.mainWindowActivated()
}})
// search field
@ -775,10 +796,14 @@ class MainFrameView {
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
model.chatFromTrustedButtonEnabled = false
model.browseFromTrustedButtonEnabled = false
} else {
model.subscribeButtonEnabled = true
model.markDistrustedButtonEnabled = true
model.markNeutralFromTrustedButtonEnabled = true
model.chatFromTrustedButtonEnabled = true
model.browseFromTrustedButtonEnabled = true
}
})
@ -827,6 +852,10 @@ class MainFrameView {
}
})
// chat tabs
def chatTabbedPane = builder.getVariable("chat-tabs")
chatTabbedPane.addChangeListener({e -> chatNotificator.serverTabChanged(e.getSource())})
// show tree by default
showSharedFilesTree.call()
@ -1029,6 +1058,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showDownloadsWindow = {
@ -1040,6 +1070,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showUploadsWindow = {
@ -1051,6 +1082,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showMonitorWindow = {
@ -1062,6 +1094,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = false
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showTrustWindow = {
@ -1073,6 +1106,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false
model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
}
def showChatWindow = {
@ -1084,6 +1118,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = false
chatNotificator.mainWindowActivated()
}
def showSharedFilesTable = {

View File

@ -280,13 +280,16 @@ class OptionsView {
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Start chat server on startup", constraints : gbc(gridx: 0, gridy: 0, anchor: GridBagConstraints.LINE_START, weightx: 100))
startChatServerCheckbox = checkBox(selected : bind{model.startChatServer}, constraints : gbc(gridx:1, gridy:0, anchor:GridBagConstraints.LINE_END))
startChatServerCheckbox = checkBox(selected : bind{model.startChatServer}, constraints : gbc(gridx:2, gridy:0, anchor:GridBagConstraints.LINE_END))
label(text : "Maximum chat connections (-1 means unlimited)", constraints : gbc(gridx: 0, gridy:1, anchor:GridBagConstraints.LINE_START, weightx:100))
maxChatConnectionsField = textField(text : bind {model.maxChatConnections}, constraints : gbc(gridx: 1, gridy : 1, anchor:GridBagConstraints.LINE_END))
maxChatConnectionsField = textField(text : bind {model.maxChatConnections}, constraints : gbc(gridx: 2, gridy : 1, anchor:GridBagConstraints.LINE_END))
label(text : "Advertise chat ability in search results", constraints : gbc(gridx: 0, gridy:2, anchor:GridBagConstraints.LINE_START, weightx:100))
advertiseChatCheckbox = checkBox(selected : bind{model.advertiseChat}, constraints : gbc(gridx:1, gridy:2, anchor:GridBagConstraints.LINE_END))
advertiseChatCheckbox = checkBox(selected : bind{model.advertiseChat}, constraints : gbc(gridx:2, gridy:2, anchor:GridBagConstraints.LINE_END))
label(text : "Maximum lines of scrollback (-1 means unlimited)", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
maxChatLinesField = textField(text : bind{model.maxChatLines}, constraints : gbc(gridx:1, gridy: 3, anchor: GridBagConstraints.LINE_END))
maxChatLinesField = textField(text : bind{model.maxChatLines}, constraints : gbc(gridx:2, gridy: 3, anchor: GridBagConstraints.LINE_END))
label(text : "Welcome message file", constraints : gbc(gridx : 0, gridy : 4, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.chatWelcomeFile}, constraints : gbc(gridx : 1, gridy : 4))
button(text : "Choose", constraints : gbc(gridx : 2, gridy : 4, anchor : GridBagConstraints.LINE_END), chooseChatFileAction)
}
panel(constraints : gbc(gridx: 0, gridy : 1, weighty: 100))
}

View File

@ -1,183 +0,0 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JPanel
import javax.swing.JTabbedPane
import javax.swing.SwingConstants
import com.muwire.core.Core
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def i
def u
def bandwidth
def trust
def retryField
def updateField
def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox
def inboundLengthField
def inboundQuantityField
def outboundLengthField
def outboundQuantityField
def i2pUDPPortField
def i2pNTCPPortField
def lnfField
def monitorCheckbox
def fontField
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def inBwField
def outBwField
def allowUntrustedCheckbox
def allowTrustListsCheckbox
def trustListIntervalField
def buttonsPanel
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:4))
button(text : "Choose", constraints : gbc(gridx : 1, gridy:4), downloadLocationAction)
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:5, gridwidth:2))
}
i = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
Core core = application.context.get("core")
if (core.router != null) {
label(text : "TCP Port", constraints : gbc(gridx :0, gridy: 5))
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:5))
label(text : "UDP Port", constraints : gbc(gridx :0, gridy: 6))
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:6))
}
}
u = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
}
bandwidth = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1))
inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2))
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2))
}
trust = builder.panel {
gridBagLayout()
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2))
label(text : "hours", constraints : gbc(gridx: 2, gridy:2))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire", p)
tabbedPane.addTab("I2P", i)
tabbedPane.addTab("GUI", u)
Core core = application.context.get("core")
if (core.router != null) {
tabbedPane.addTab("Bandwidth", bandwidth)
}
tabbedPane.addTab("Trust", trust)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())
panel.add(tabbedPane, BorderLayout.CENTER)
panel.add(buttonsPanel, BorderLayout.SOUTH)
d.getContentPane().add(panel)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

View File

@ -0,0 +1,108 @@
package com.muwire.gui
import java.awt.Taskbar
import java.awt.Taskbar.Feature
import javax.swing.JPanel
import javax.swing.JTabbedPane
import griffon.core.mvc.MVCGroupManager
class ChatNotificator {
public static interface Listener {
void update()
}
private final MVCGroupManager groupManager
private boolean chatInFocus
private String currentServerTab
private String currentRoomTab
private final Map<String, Integer> roomsWithMessages = new HashMap<>()
private Listener listener
ChatNotificator(MVCGroupManager groupManager) {
this.groupManager = groupManager
}
void serverTabChanged(JTabbedPane source) {
JPanel panel = source.getSelectedComponent()
if (panel == null) {
currentServerTab = null
currentRoomTab = null
return
}
String mvcId = panel.getClientProperty("mvcId")
def group = groupManager.getGroups().get(mvcId)
JTabbedPane childPane = panel.getClientProperty("childPane")
JPanel roomPanel = childPane.getSelectedComponent()
currentServerTab = mvcId
currentRoomTab = childPane.getSelectedComponent()?.getClientProperty("mvcId")
if (currentRoomTab != null) {
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
}
void roomTabChanged(JTabbedPane source) {
JPanel panel = source.getSelectedComponent()
if (panel == null) {
currentRoomTab = null
return
}
currentRoomTab = panel.getClientProperty("mvcId")
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
void roomClosed(String mvcId) {
roomsWithMessages.remove(mvcId)
updateBadge()
}
void mainWindowDeactivated() {
chatInFocus = false
}
void mainWindowActivated() {
chatInFocus = true
if (currentRoomTab != null)
roomsWithMessages.remove(currentRoomTab)
updateBadge()
}
void onMessage(String roomId) {
if (roomId != currentRoomTab || !chatInFocus) {
Integer previous = roomsWithMessages[roomId]
if (previous == null)
roomsWithMessages[roomId] = 1
else
roomsWithMessages[roomId] = previous + 1
}
updateBadge()
}
private void updateBadge() {
listener?.update()
if (!Taskbar.isTaskbarSupported())
return
def taskBar = Taskbar.getTaskbar()
if (!taskBar.isSupported(Feature.ICON_BADGE_NUMBER))
return
if (roomsWithMessages.isEmpty())
taskBar.setIconBadge("")
else {
int total = 0
roomsWithMessages.values().each {
total += it
}
taskBar.setIconBadge(String.valueOf(total))
}
}
}

View File

@ -0,0 +1,174 @@
package com.muwire.gui;
import java.awt.Component;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
/**
* The SmartScroller will attempt to keep the viewport positioned based on
* the users interaction with the scrollbar. The normal behaviour is to keep
* the viewport positioned to see new data as it is dynamically added.
*
* Assuming vertical scrolling and data is added to the bottom:
*
* - when the viewport is at the bottom and new data is added,
* then automatically scroll the viewport to the bottom
* - when the viewport is not at the bottom and new data is added,
* then do nothing with the viewport
*
* Assuming vertical scrolling and data is added to the top:
*
* - when the viewport is at the top and new data is added,
* then do nothing with the viewport
* - when the viewport is not at the top and new data is added, then adjust
* the viewport to the relative position it was at before the data was added
*
* Similiar logic would apply for horizontal scrolling.
*/
public class SmartScroller implements AdjustmentListener
{
public final static int HORIZONTAL = 0;
public final static int VERTICAL = 1;
public final static int START = 0;
public final static int END = 1;
private int viewportPosition;
private JScrollBar scrollBar;
private boolean adjustScrollBar = true;
private int previousValue = -1;
private int previousMaximum = -1;
/**
* Convenience constructor.
* Scroll direction is VERTICAL and viewport position is at the END.
*
* @param scrollPane the scroll pane to monitor
*/
public SmartScroller(JScrollPane scrollPane)
{
this(scrollPane, VERTICAL, END);
}
/**
* Convenience constructor.
* Scroll direction is VERTICAL.
*
* @param scrollPane the scroll pane to monitor
* @param viewportPosition valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int viewportPosition)
{
this(scrollPane, VERTICAL, viewportPosition);
}
/**
* Specify how the SmartScroller will function.
*
* @param scrollPane the scroll pane to monitor
* @param scrollDirection indicates which JScrollBar to monitor.
* Valid values are HORIZONTAL and VERTICAL.
* @param viewportPosition indicates where the viewport will normally be
* positioned as data is added.
* Valid values are START and END
*/
public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition)
{
if (scrollDirection != HORIZONTAL
&& scrollDirection != VERTICAL)
throw new IllegalArgumentException("invalid scroll direction specified");
if (viewportPosition != START
&& viewportPosition != END)
throw new IllegalArgumentException("invalid viewport position specified");
this.viewportPosition = viewportPosition;
if (scrollDirection == HORIZONTAL)
scrollBar = scrollPane.getHorizontalScrollBar();
else
scrollBar = scrollPane.getVerticalScrollBar();
scrollBar.addAdjustmentListener( this );
// Turn off automatic scrolling for text components
Component view = scrollPane.getViewport().getView();
if (view instanceof JTextComponent)
{
JTextComponent textComponent = (JTextComponent)view;
DefaultCaret caret = (DefaultCaret)textComponent.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
}
@Override
public void adjustmentValueChanged(final AdjustmentEvent e)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
checkScrollBar(e);
}
});
}
/*
* Analyze every adjustment event to determine when the viewport
* needs to be repositioned.
*/
private void checkScrollBar(AdjustmentEvent e)
{
// The scroll bar listModel contains information needed to determine
// whether the viewport should be repositioned or not.
JScrollBar scrollBar = (JScrollBar)e.getSource();
BoundedRangeModel listModel = scrollBar.getModel();
int value = listModel.getValue();
int extent = listModel.getExtent();
int maximum = listModel.getMaximum();
boolean valueChanged = previousValue != value;
boolean maximumChanged = previousMaximum != maximum;
// Check if the user has manually repositioned the scrollbar
if (valueChanged && !maximumChanged)
{
if (viewportPosition == START)
adjustScrollBar = value != 0;
else
adjustScrollBar = value + extent >= maximum;
}
// Reset the "value" so we can reposition the viewport and
// distinguish between a user scroll and a program scroll.
// (ie. valueChanged will be false on a program scroll)
if (adjustScrollBar && viewportPosition == END)
{
// Scroll the viewport to the end.
scrollBar.removeAdjustmentListener( this );
value = maximum - extent;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
if (adjustScrollBar && viewportPosition == START)
{
// Keep the viewport at the same relative viewportPosition
scrollBar.removeAdjustmentListener( this );
value = value + maximum - previousMaximum;
scrollBar.setValue( value );
scrollBar.addAdjustmentListener( this );
}
previousValue = value;
previousMaximum = maximum;
}
}

View File

@ -3,6 +3,7 @@ mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
}

BIN
images/i2cp_config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,2 +1,5 @@
apply plugin: 'application'
mainClassName = 'com.muwire.pinger.Pinger'
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
}

View File

@ -47,6 +47,17 @@ task pluginConfig {
}
}
task clientsConfig {
doLast {
def binding = [ "libname" : libDirPath(), "version" : project.version ]
def templateFile = new File("$projectDir/templates/clients.config.template")
def engine = new groovy.text.SimpleTemplateEngine()
def output = engine.createTemplate(templateFile).make(binding)
def outputFile = new File(zipDir, "clients.config")
outputFile.text = output
}
}
task pluginDir {
dependsOn ':webui:assemble'
doLast { task ->
@ -59,10 +70,13 @@ task pluginDir {
java.nio.file.Files.copy(it.toPath(), dest.toPath())
}
}
def jarFile = webapp.configurations.jarArtifact.getAllArtifacts().file[0]
def dest = new File(libDir, jarFile.getName())
java.nio.file.Files.copy(jarFile.toPath(), dest.toPath())
webAppDir.mkdirs()
def warFile = webapp.configurations.warArtifact.getAllArtifacts().file[0]
def dest = new File(webAppDir, "MuWire.war")
dest = new File(webAppDir, "MuWire.war")
java.nio.file.Files.copy(warFile.toPath(), dest.toPath())
"zip -d ${dest.toString()} *.jar".execute()
}
@ -80,7 +94,7 @@ task pack {
doLast {
if (project.pack200 == "true") {
libDir.listFiles().stream().filter( { it.getName().endsWith(".jar") } ).
filter({it.length() > 512*1024}).forEach {
forEach {
println "packing $it"
def name = it.toString()
println "pack200 --no-gzip ${name}.pack $name".execute().text
@ -117,6 +131,7 @@ task sign {
}
webappConfig.dependsOn pluginDir
clientsConfig.dependsOn webappConfig
pack.dependsOn webappConfig
pluginZip.dependsOn(webappConfig,pluginConfig,pack)
sign.dependsOn pluginZip

View File

@ -0,0 +1,6 @@
clientApp.0.main=com.muwire.webui.MuWireClient
clientApp.0.name=MuWire
clientApp.0.startOnLoad=true
clientApp.0.delay=5
clientApp.0.classpath=${libname}
clientApp.0.args=version=${version} home="\$PLUGIN"

View File

@ -7,8 +7,9 @@ signer=${signer}
websiteURL=${websiteURL}
updateURL.su3=${updateURLsu3}
min-i2p-version=${i2pVersion}
min-java-version=1.8
min-java-version=8
consoleLinkName=MuWire
consoleLinkTooltip=Anonymous File Sharing
consoleLinkURL=/MuWire
consoleLinkURL=/MuWire/
console-icon=images/muwire.png
<% println "date="+System.currentTimeMillis() %>

View File

@ -1,3 +1,6 @@
apply plugin : 'application'
mainClassName = 'com.muwire.update.UpdateServer'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
}

12
webui/.gitignore vendored
View File

@ -1,12 +0,0 @@
Thumbs.db
.DS_Store
.gradle
build/
out/
.idea
*.iml
*.ipr
*.iws
.project
.settings
.classpath

View File

@ -1,109 +1,117 @@
buildscript {
repositories {
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
classpath "org.grails:grails-gradle-plugin:$grailsVersion"
classpath "org.grails.plugins:hibernate5:7.0.0"
classpath "gradle.plugin.com.github.erdi.webdriver-binaries:webdriver-binaries-gradle-plugin:2.0"
classpath "com.bertramlabs.plugins:asset-pipeline-gradle:3.0.10"
}
}
version "0.1"
group "webui"
apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"com.github.erdi.webdriver-binaries"
apply plugin:"org.grails.grails-gsp"
apply plugin:"com.bertramlabs.asset-pipeline"
repositories {
maven { url "https://repo.grails.org/grails/core" }
}
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
warArtifact
jarArtifact
}
apply plugin : 'war'
dependencies {
compile project(":core")
developmentOnly("org.springframework.boot:spring-boot-devtools")
compile "org.springframework.boot:spring-boot-starter-logging"
compile "org.springframework.boot:spring-boot-autoconfigure"
compile "org.grails:grails-core"
compile "org.springframework.boot:spring-boot-starter-actuator"
provided "org.springframework.boot:spring-boot-starter-tomcat"
compile "org.grails:grails-web-boot"
compile "org.grails:grails-logging"
compile "org.grails:grails-plugin-rest"
compile "org.grails:grails-plugin-databinding"
compile "org.grails:grails-plugin-i18n"
compile "org.grails:grails-plugin-services"
compile "org.grails:grails-plugin-url-mappings"
compile "org.grails:grails-plugin-interceptors"
compile "org.grails.plugins:cache"
compile "org.grails.plugins:async"
compile "org.grails.plugins:scaffolding"
compile "org.grails.plugins:events"
compile "org.grails.plugins:hibernate5"
compile "org.hibernate:hibernate-core:5.4.0.Final"
compile "org.grails.plugins:gsp"
compileOnly "io.micronaut:micronaut-inject-groovy"
console "org.grails:grails-console"
profile "org.grails.profiles:web"
runtime "org.glassfish.web:el-impl:2.1.2-b03"
runtime "com.h2database:h2"
runtime "org.apache.tomcat:tomcat-jdbc"
runtime "javax.xml.bind:jaxb-api:2.3.0"
runtime "com.bertramlabs.plugins:asset-pipeline-grails:3.0.10"
testCompile "org.grails:grails-gorm-testing-support"
testCompile "org.mockito:mockito-core"
testCompile "org.grails:grails-web-testing-support"
testCompile "org.grails.plugins:geb"
testCompile "org.seleniumhq.selenium:selenium-remote-driver:3.14.0"
testCompile "org.seleniumhq.selenium:selenium-api:3.14.0"
testCompile "org.seleniumhq.selenium:selenium-support:3.14.0"
testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.14.0"
testRuntime "org.seleniumhq.selenium:selenium-firefox-driver:3.14.0"
providedCompile(project(':core')) {
transitive = false
}
compile fileTree("../i2pjars") { include '*.jar' }
}
bootRun {
jvmArgs(
'-Dspring.output.ansi.enabled=always',
'-noverify',
'-XX:TieredStopAtLevel=1',
'-Xmx1024m')
sourceResources sourceSets.main
String springProfilesActive = 'spring.profiles.active'
systemProperty springProfilesActive, System.getProperty(springProfilesActive)
war {
from 'src/main/css'
from ('src/main/images', {
into "images"
})
from ('src/main/js', {
into "js"
})
from ('src/main/resources', {
into "WEB-INF/classes/com/muwire/webui"
})
webInf {
from "$buildDir/compiledJsps"
into "classes"
}
excludes = new HashSet(['**/*.jsp', '**/*.jsi'])
webXml = file("$buildDir/tmp_jsp/web.xml")
}
webdriverBinaries {
chromedriver '2.45.0'
geckodriver '0.24.0'
task precompileJsp {
doLast {
ant.taskdef (name : 'jasper',
classname: 'org.apache.jasper.JspC',
classpath: configurations.compile.asPath)
def generated = new File("$buildDir/tmp_jsp")
generated.mkdirs()
ant.jasper(package: 'com.muwire.webui',
classPath : sourceSets.main.runtimeClasspath.asPath,
uriroot: webAppDir,
outputDir: "$buildDir/tmp_jsp",
compilerSourceVM: project.sourceCompatibility,
compilerTargetVM: project.targetCompatibility,
webXmlFragment: "$buildDir/tmp_jsp/web.xml.jasper")
def output = new File("$buildDir/compiledJsps")
output.mkdirs()
ant.javac(srcDir: 'build/tmp_jsp',
classPath : sourceSets.main.runtimeClasspath.asPath,
debug : true,
includeAntRuntime : false,
deprecation : "on",
source: project.sourceCompatibility,
target: project.targetCompatibility,
destDir:file("$buildDir/compiledJsps"))
}
}
tasks.withType(Test) {
systemProperty "geb.env", System.getProperty('geb.env')
systemProperty "geb.build.reportsDir", reporting.file("geb/integrationTest")
systemProperty "webdriver.chrome.driver", System.getProperty('webdriver.chrome.driver')
systemProperty "webdriver.gecko.driver", System.getProperty('webdriver.gecko.driver')
task generateWebXML {
doLast {
def template = new File("$projectDir/templates/web.xml.template")
def templateText = template.text
def jasper = new File("$buildDir/tmp_jsp/web.xml.jasper")
templateText = templateText.replaceAll("__JASPER__", jasper.text)
templateText = templateText.replaceAll("__VERSION__", project.version)
def webXml = new File("$buildDir/tmp_jsp/web.xml")
webXml.text = templateText
}
}
assets {
minifyJs = true
minifyCss = true
// compile the po files and put them in the jar
task bundle {
doLast {
// run bundle-messages.sh
println 'starting bundle-messages'
println "webui/bundle-messages.sh".execute().text
println 'finished bundle-messages'
// compile java files in build/messages-src
ant.mkdir(dir: "$buildDir/compiledMessages")
ant.javac(srcDir: "$buildDir/messages-src",
classPath : sourceSets.main.runtimeClasspath.asPath,
debug : false,
includeAntRuntime : false,
source: project.sourceCompatibility,
target: project.targetCompatibility,
destDir:file("$buildDir/compiledMessages"))
// add resulting classes to build/libs/webui-(version).jar
ant.jar(destfile: "$buildDir/libs/webui-${version}.jar",
basedir: "$buildDir/compiledMessages",
includes: '**/messages_*.class',
update: 'true')
}
}
// rebuild the english po file for uploading to transifex
task poupdate {
doLast {
// run bundle-messages.sh
println 'starting bundle-messages -p'
println "webui/bundle-messages.sh -p".execute().text
println 'finished bundle-messages -p'
}
}
precompileJsp.dependsOn compileJava
generateWebXML.dependsOn precompileJsp
bundle.dependsOn precompileJsp
poupdate.dependsOn precompileJsp
war.dependsOn generateWebXML, bundle
artifacts {
warArtifact war
jarArtifact jar
}

143
webui/bundle-messages.sh Executable file
View File

@ -0,0 +1,143 @@
#!/bin/sh
#
# Update messages_xx.po and messages_xx.class files,
# from both java and jsp sources.
# Requires installed programs xgettext, msgfmt, msgmerge, and find.
#
# usage:
# bundle-messages.sh (generates the resource bundle from the .po file)
# bundle-messages.sh -p (updates the .po file from the source tags, then generates the resource bundle)
#
# zzz - public domain
#
cd `dirname $0`
echo "bundle messages in $PWD"
CLASS=com.muwire.webui.messages
TMPFILE=build/javafiles.txt
export TZ=UTC
RC=0
if ! $(which javac > /dev/null 2>&1); then
export JAVAC=${JAVA_HOME}/../bin/javac
fi
if [ "$1" = "-p" ]
then
POUPDATE=1
LG2=en
fi
# on windows, one must specify the path of commnad find
# since windows has its own version of find.
if which find|grep -q -i windows ; then
export PATH=.:/bin:/usr/local/bin:$PATH
fi
# Fast mode - update ondemond
# set LG2 to the language you need in environment variables to enable this
# add ../src/ so the refs will work in the po file
JPATHS="src/main/java/ build/tmp_jsp"
for i in locale/messages_*.po
do
# get language
LG=${i#locale/messages_}
LG=${LG%.po}
# skip, if specified
if [ $LG2 ]; then
[ $LG != $LG2 ] && continue || echo INFO: Language update is set to [$LG2] only.
fi
if [ "$POUPDATE" = "1" ]
then
# make list of java files newer than the .po file
find $JPATHS -name *.java -newer $i > $TMPFILE
fi
if [ -s build/compiledJsps/com/muwire/webui/messages/messages_$LG.class -a \
build/compiledJsps/com/muwire/webui/messages/messages_$LG.class -nt $i -a \
! -s $TMPFILE ]
then
continue
fi
if [ "$POUPDATE" = "1" ]
then
echo "Updating the $i file from the tags..."
# extract strings from java and jsp files, and update messages.po files
# translate calls must be one of the forms:
# _("foo")
# _t("foo")
# _x("foo")
# intl._t("foo")
# In a jsp, you must use a helper or handler that has the context set.
# To start a new translation, copy the header from an old translation to the new .po file,
# then ant distclean updater.
find $JPATHS -name *.java > $TMPFILE
xgettext -f $TMPFILE -F -L java --from-code=UTF-8 --add-comments\
--keyword=_ --keyword=_t --keyword=_x --keyword=intl._ --keyword=intl.title \
-o ${i}t
if [ $? -ne 0 ]
then
echo "ERROR - xgettext failed on ${i}, not updating translations"
rm -f ${i}t
RC=1
break
fi
msgmerge -U --backup=none $i ${i}t
if [ $? -ne 0 ]
then
echo "ERROR - msgmerge failed on ${i}, not updating translations"
rm -f ${i}t
RC=1
break
fi
rm -f ${i}t
# so we don't do this again
touch $i
fi
if [ "$LG" != "en" ]
then
# only generate for non-source language
echo "Generating ${CLASS}_$LG ResourceBundle..."
msgfmt -V | grep -q -E ' 0\.((19)|[2-9])'
if [ $? -ne 0 ]
then
# slow way
# convert to class files in WEB-INF/classes
msgfmt --java --statistics -r $CLASS -l $LG -d build/compiledJsps $i
if [ $? -ne 0 ]
then
echo "ERROR - msgfmt failed on ${i}, not updating translations"
# msgfmt leaves the class file there so the build would work the next time
find build/compiledJsps -name messages_${LG}.class -exec rm -f {} \;
RC=1
break
fi
else
# fast way
# convert to java files in build/messages-src
TD=build/messages-src-tmp/
TDX=$TD/com/muwire/webui
TD2=build/messages-src
TDY=$TD2/com/muwire/webui
rm -rf $TD
mkdir -p $TD $TDY
msgfmt --java --statistics --source -r $CLASS -l $LG -d $TD $i
if [ $? -ne 0 ]
then
echo "ERROR - msgfmt failed on ${i}, not updating translations"
# msgfmt leaves the class file there so the build would work the next time
find WEB-INF/classes -name messages_${LG}.class -exec rm -f {} \;
RC=1
break
fi
mv $TDX/messages_$LG.java $TDY
rm -rf $TD
fi
fi
done
rm -f $TMPFILE
exit $RC

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve">
<g>
<g>
<circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.79" cy="46.789" r="45.374"/>
</g>
<g>
<path fill="#FEB672" d="M71.126,29.576c0,0.414-0.337,0.75-0.75,0.75h-3.25v3.25c0,0.415-0.337,0.751-0.751,0.751h-1.499
c-0.415,0-0.75-0.336-0.75-0.751v-3.25h-3.251c-0.414,0-0.749-0.336-0.749-0.75v-1.498c0-0.416,0.335-0.752,0.749-0.752h3.251
v-3.249c0-0.414,0.335-0.75,0.75-0.75h1.499c0.414,0,0.751,0.336,0.751,0.75v3.249h3.25c0.413,0,0.75,0.336,0.75,0.752V29.576z"/>
</g>
<path fill="#FEB672" d="M50.42,60.386c0.554,1.467,0.855,1.951,1.493,3.44c0.271,0.627,0.523,1.228,0.649,1.518
c0.049,0.117,0.036,0.248-0.033,0.355c-0.172,0.259-0.552,0.747-1.181,1.086c-1.098,0.594-3.409,0.809-4.555,0.812h-0.006
c-1.146-0.004-3.457-0.219-4.558-0.812c-0.627-0.339-1.006-0.827-1.177-1.086c-0.07-0.107-0.083-0.238-0.032-0.355
c0.123-0.29,0.376-0.891,0.646-1.518c0.64-1.489,0.941-1.974,1.495-3.44c0.485-1.294,0.729-3.175,0.745-4.593
c0.006-0.604-0.03-1.122-0.106-1.476c-0.121-0.56-0.501-1.412-0.907-2.042c-0.548-0.849-1.527-1.583-2.157-1.919
c-0.475-0.254-1.984-0.817-2.576-1.146c-0.755-0.416-1.739-1.067-2.399-1.584c-0.735-0.574-2.182-1.992-2.746-2.695
c-1.084-1.344-2.083-2.922-2.565-4.62c-0.601-2.106-0.576-3.009-0.657-3.688c-0.014-0.117,0.075-0.222,0.191-0.227
c0.73-0.025,3.854-0.093,16.809-0.081c12.953-0.012,16.076,0.056,16.806,0.081c0.118,0.005,0.206,0.109,0.191,0.227
c-0.08,0.68-0.057,1.582-0.654,3.688c-0.486,1.698-1.483,3.276-2.567,4.62c-0.564,0.703-2.011,2.121-2.746,2.695
c-0.661,0.517-1.646,1.168-2.399,1.584c-0.594,0.328-2.102,0.892-2.576,1.146c-0.63,0.336-1.608,1.07-2.158,1.919
c-0.405,0.63-0.785,1.482-0.904,2.042c-0.079,0.354-0.112,0.872-0.107,1.476C49.69,57.211,49.935,59.092,50.42,60.386z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="93.58px" height="93.58px" viewBox="0 0 93.58 93.58" enable-background="new 0 0 93.58 93.58" xml:space="preserve">
<g>
<g>
<circle fill="none" stroke="#FEB672" stroke-width="2.8347" stroke-miterlimit="10" cx="46.88" cy="46.792" r="45.374"/>
</g>
<path fill="#FEB672" d="M64.379,40.958v24.062c0,1.208-0.979,2.188-2.188,2.188H31.567c-1.208,0-2.188-0.979-2.188-2.188V28.562
c0-1.208,0.98-2.188,2.188-2.188h18.229v12.396c0,1.208,0.979,2.188,2.188,2.188H64.379z M55.629,44.604
c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729v1.458c0,0.41,0.319,0.729,0.729,0.729H54.9
c0.41,0,0.729-0.319,0.729-0.729V44.604z M55.629,50.438c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729
v1.458c0,0.41,0.319,0.729,0.729,0.729H54.9c0.41,0,0.729-0.319,0.729-0.729V50.438z M55.629,56.271
c0-0.41-0.318-0.729-0.729-0.729H38.858c-0.41,0-0.729,0.319-0.729,0.729v1.458c0,0.41,0.319,0.729,0.729,0.729H54.9
c0.41,0,0.729-0.319,0.729-0.729V56.271z M63.468,38.042H52.713V27.287c0.318,0.205,0.592,0.41,0.82,0.638l9.297,9.297
C63.059,37.449,63.264,37.723,63.468,38.042z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

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