Compare commits
444 Commits
muwire-0.6
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c11d81c6c3 | ||
![]() |
ee5e90c4ab | ||
![]() |
64d2a87d26 | ||
![]() |
f0304dbe7d | ||
![]() |
bdad8d9309 | ||
![]() |
8c110bbae5 | ||
![]() |
2cc1e384bc | ||
![]() |
9337d1b74d | ||
![]() |
16ed5dd346 | ||
![]() |
7b55fc9ed8 | ||
![]() |
d5c8050572 | ||
![]() |
83546d68d2 | ||
a891c83518 | |||
aa56cc23c0 | |||
a2b37ef567 | |||
4bc04ae631 | |||
56da9a16b0 | |||
2935ee1a1d | |||
855183397b | |||
e27704c1af | |||
5c18b4a141 | |||
dcd233b7ad | |||
7cee8a28ba | |||
7446fc949a | |||
598ab90f63 | |||
043028c296 | |||
cd1757fac3 | |||
9d4b365e63 | |||
![]() |
b12d57e30a | ||
![]() |
f33d1b6db3 | ||
![]() |
9e451460da | ||
![]() |
ffa52c129a | ||
b779fb75a0 | |||
fbe6b53278 | |||
b2bd95788d | |||
83d4a2624b | |||
03e20e21aa | |||
8a08955675 | |||
4ec54ebe54 | |||
758af6f48e | |||
a7bdd47fcd | |||
f7caa77a18 | |||
7641f64536 | |||
02baaace48 | |||
![]() |
d90067ff39 | ||
c910a215f5 | |||
65e073b1b9 | |||
489a7518c3 | |||
3733e48bbd | |||
c3723a1348 | |||
0e0f52bc77 | |||
60b9e990cf | |||
28ad0ae30f | |||
9142de85cd | |||
4eb31c11e3 | |||
e8afe358a5 | |||
![]() |
3db4317fc1 | ||
![]() |
5ad2b28527 | ||
![]() |
3036765f81 | ||
![]() |
8f9b1e5a8b | ||
![]() |
e6d59a2438 | ||
![]() |
32609b4779 | ||
![]() |
74ac4cfecf | ||
![]() |
69173c4156 | ||
![]() |
6283287bee | ||
![]() |
8e3f76f68c | ||
![]() |
574294fdc6 | ||
![]() |
8bd41546cd | ||
![]() |
ba5425c958 | ||
![]() |
22580f002c | ||
![]() |
5c773cec80 | ||
![]() |
7df00e6709 | ||
![]() |
5c05bd2562 | ||
![]() |
9df1d043e4 | ||
![]() |
6ea1a15641 | ||
![]() |
c0575facec | ||
![]() |
09168844e0 | ||
![]() |
e21d482393 | ||
![]() |
f5fc3e40c2 | ||
![]() |
796a0138fa | ||
![]() |
505b4ddb06 | ||
![]() |
a35216ff56 | ||
![]() |
fba92fe9b9 | ||
![]() |
1cc511b0ae | ||
![]() |
fa94c8ebfa | ||
![]() |
88b68a3c5c | ||
![]() |
b3e0d2ee7a | ||
![]() |
ce293cbda8 | ||
![]() |
3abc617e9f | ||
![]() |
67ee634f20 | ||
![]() |
503d54927f | ||
![]() |
5788329e1a | ||
![]() |
f0ffc68122 | ||
![]() |
3d710cebe5 | ||
![]() |
7d67573c92 | ||
![]() |
3acc676448 | ||
![]() |
2bf03b6b84 | ||
![]() |
b8ba6df4d5 | ||
![]() |
9fa7fa07b4 | ||
![]() |
1c7253ea0a | ||
![]() |
d947ad2997 | ||
![]() |
dd0bd6f5f8 | ||
![]() |
f05b6d0b40 | ||
![]() |
906c69a482 | ||
![]() |
5375b7aec0 | ||
![]() |
ea5da2431a | ||
![]() |
14b3a9ac9e | ||
![]() |
40bbef4583 | ||
![]() |
f811653247 | ||
![]() |
f321000071 | ||
![]() |
6eb85283cd | ||
![]() |
2973759cd9 | ||
![]() |
fe945a9941 | ||
![]() |
5f7e949310 | ||
![]() |
11edb2cb3c | ||
![]() |
ff1f801155 | ||
![]() |
0a98083c64 | ||
![]() |
75b2852f6e | ||
![]() |
5774cdee94 | ||
![]() |
2b0f4e52ca | ||
![]() |
1d20dc917b | ||
![]() |
63e3b3710c | ||
![]() |
0878b89082 | ||
![]() |
fecf0ecae8 | ||
![]() |
fec8d4ef9f | ||
![]() |
067ac8582a | ||
![]() |
31cac25a23 | ||
![]() |
6bcc44e01e | ||
![]() |
31652b34d7 | ||
![]() |
41a15fc7d5 | ||
![]() |
da3d7d7a50 | ||
![]() |
3a079d9f21 | ||
![]() |
ba0c85fe07 | ||
![]() |
ecb2283886 | ||
![]() |
cf9a18cee5 | ||
![]() |
982a93a04b | ||
![]() |
58137d11d1 | ||
![]() |
d87bec927d | ||
![]() |
dc8dd96495 | ||
![]() |
add9fb6feb | ||
![]() |
c500e95ab6 | ||
![]() |
477c3285d2 | ||
![]() |
1f5b112bfe | ||
![]() |
b0d09853e4 | ||
![]() |
b96d997037 | ||
![]() |
a631ec1e14 | ||
![]() |
62a06bc891 | ||
![]() |
3534b23194 | ||
![]() |
c561ae9140 | ||
![]() |
5926457eb5 | ||
![]() |
37c93e352b | ||
![]() |
be8fecda39 | ||
![]() |
7ec6257ac0 | ||
![]() |
c4ea58c330 | ||
![]() |
a482fe5c93 | ||
![]() |
2ee84848c4 | ||
![]() |
e29d7f6872 | ||
![]() |
5ded824ef2 | ||
![]() |
c607560cb8 | ||
![]() |
8b341bb125 | ||
![]() |
6bc5a9075b | ||
![]() |
6b1d2bc5ce | ||
![]() |
0cbbaf6a63 | ||
![]() |
3363b99675 | ||
![]() |
4ab4785539 | ||
![]() |
e595fa97e8 | ||
![]() |
65a7088463 | ||
![]() |
2d5bd653c1 | ||
![]() |
a864343c05 | ||
![]() |
696b348469 | ||
![]() |
b08333c5ea | ||
![]() |
0cf368c1af | ||
![]() |
62ab957892 | ||
![]() |
2b9e722165 | ||
![]() |
8cf4b23762 | ||
![]() |
1285c68521 | ||
![]() |
daa9e0bafc | ||
![]() |
8efd9c2c88 | ||
![]() |
918549f164 | ||
![]() |
e30a4666cb | ||
![]() |
26167abc08 | ||
![]() |
93f7c67f37 | ||
![]() |
f9a0a5e08a | ||
![]() |
d8ae275df2 | ||
![]() |
fce879be5d | ||
![]() |
0b58e22714 | ||
![]() |
dd230c4dfc | ||
![]() |
fba0b001c0 | ||
![]() |
6978c7b992 | ||
![]() |
7355e76e1b | ||
![]() |
5147cf21a0 | ||
![]() |
e8dd7d710d | ||
![]() |
fc9114eaa5 | ||
![]() |
20b7104c41 | ||
![]() |
570616951a | ||
![]() |
e075bfac55 | ||
![]() |
b6411a555c | ||
![]() |
d395475727 | ||
![]() |
8ae0a16b8a | ||
![]() |
38fcdfc97a | ||
![]() |
a0fb07cf99 | ||
![]() |
3747f9a5d5 | ||
![]() |
3a738f8f62 | ||
![]() |
ca56363438 | ||
![]() |
e06cb05e2a | ||
![]() |
8ab2dd7900 | ||
![]() |
26116d313a | ||
![]() |
738f177d6c | ||
![]() |
62c4579bbd | ||
![]() |
18d84685ec | ||
![]() |
c05a7a021c | ||
![]() |
a9935eba62 | ||
![]() |
e3d80bf809 | ||
![]() |
a59a1d3f30 | ||
![]() |
37ed75a3e8 | ||
![]() |
cd4b600ba2 | ||
![]() |
fcd6dbcfbd | ||
![]() |
f3ab15bd74 | ||
![]() |
cddaad0f29 | ||
![]() |
ecb597e0a0 | ||
![]() |
ec2a934f73 | ||
![]() |
e1d630fdee | ||
![]() |
5807672503 | ||
![]() |
2fadb314d3 | ||
![]() |
ec5c15ff64 | ||
![]() |
c169a7613f | ||
![]() |
0f762968ae | ||
![]() |
8e6517e7d8 | ||
![]() |
6946bff7f9 | ||
![]() |
37dcedb99b | ||
![]() |
afb92b0e4e | ||
![]() |
7c39dff34f | ||
![]() |
e41c122d2d | ||
![]() |
117c5eaf67 | ||
![]() |
10fab2b47f | ||
![]() |
3f71df3d29 | ||
![]() |
813e211200 | ||
![]() |
1adb130fba | ||
![]() |
f69d4027db | ||
![]() |
e0d006ec69 | ||
![]() |
81d8af57ed | ||
![]() |
42c48a8e37 | ||
![]() |
3b1349b643 | ||
![]() |
0250ea329c | ||
![]() |
b722c64ad8 | ||
![]() |
effa3b567e | ||
![]() |
64f198d599 | ||
![]() |
131b2defbb | ||
![]() |
df5aab67ac | ||
![]() |
fdc030904c | ||
![]() |
2a4fae8de4 | ||
![]() |
662b065116 | ||
![]() |
300938fa44 | ||
![]() |
086e27876d | ||
![]() |
247c62bfb4 | ||
![]() |
a13315c324 | ||
![]() |
65f40ef23a | ||
![]() |
96a611ff78 | ||
![]() |
0f4119b74f | ||
![]() |
6847329093 | ||
![]() |
9d2bcf70c7 | ||
![]() |
aa33709f04 | ||
![]() |
eacaedaf3d | ||
![]() |
f9c428cfcd | ||
![]() |
aa1ede46d2 | ||
![]() |
3c43244631 | ||
![]() |
b468a6f19b | ||
![]() |
cfdc750ac0 | ||
![]() |
6f8b006227 | ||
![]() |
3f4bf986f3 | ||
![]() |
bef1033e12 | ||
![]() |
13061d60a4 | ||
![]() |
5c6917a7e6 | ||
![]() |
2ec15cfbbc | ||
![]() |
1325a8dc65 | ||
![]() |
b5d8fcf25b | ||
![]() |
c22ff0678e | ||
![]() |
07051b813a | ||
![]() |
5c22af6576 | ||
![]() |
c3e1298ea3 | ||
![]() |
949b616fdd | ||
![]() |
2b1d95e2ef | ||
![]() |
3d967da110 | ||
![]() |
66fde32b64 | ||
![]() |
80a89a5ac0 | ||
![]() |
c59e038c2a | ||
![]() |
844bd8fd6e | ||
![]() |
7d9ebb5b0b | ||
![]() |
7fd7444dbf | ||
![]() |
13af6cce22 | ||
![]() |
458dbec5fd | ||
![]() |
2137d6d30b | ||
![]() |
b28de0c119 | ||
![]() |
0fd4695b7c | ||
![]() |
74dddc4da4 | ||
![]() |
8bff987d30 | ||
![]() |
de8684bafc | ||
![]() |
905f559aa9 | ||
![]() |
c7f57c0b15 | ||
![]() |
0f0f46f425 | ||
![]() |
d6a3c8b24c | ||
![]() |
8c661ca1ae | ||
![]() |
f579c8754f | ||
![]() |
5c17536683 | ||
![]() |
8536353c26 | ||
![]() |
84375c0201 | ||
![]() |
9c0c187a18 | ||
![]() |
8ae735e5c0 | ||
![]() |
8224dda3fd | ||
![]() |
c852d7474e | ||
![]() |
71685d2052 | ||
![]() |
e57e513ca1 | ||
![]() |
aa4fb14540 | ||
![]() |
5f74abc944 | ||
![]() |
c4135389a4 | ||
![]() |
a6e0834722 | ||
![]() |
bc628b9c00 | ||
![]() |
9b2669a8b8 | ||
![]() |
a0f70f7677 | ||
![]() |
23b2c912e2 | ||
![]() |
ecfd4180c0 | ||
![]() |
42489ba6b2 | ||
![]() |
61207f893d | ||
![]() |
4e32359718 | ||
![]() |
8d4af48eca | ||
![]() |
693f63534d | ||
![]() |
b057e848d0 | ||
![]() |
0114224d1f | ||
![]() |
beab2be713 | ||
![]() |
edd4a1ff4b | ||
![]() |
85814b7544 | ||
![]() |
d46fbd66f0 | ||
![]() |
06bd9c80e8 | ||
![]() |
54b8628435 | ||
![]() |
b37a548771 | ||
![]() |
a14689acff | ||
![]() |
a73bc956bf | ||
![]() |
d595a768b8 | ||
![]() |
0fd6421fae | ||
![]() |
6e9a36461a | ||
![]() |
d115f54812 | ||
![]() |
f627f661f2 | ||
![]() |
0e7ec3dfb3 | ||
![]() |
0188bd34a9 | ||
![]() |
a2becfa6e2 | ||
![]() |
ea32af9b91 | ||
![]() |
c74c26e4c6 | ||
![]() |
382e21225b | ||
![]() |
81c406cbf6 | ||
![]() |
d9eb46d65c | ||
![]() |
dadfed20f1 | ||
![]() |
6dad29a772 | ||
![]() |
884253fe29 | ||
![]() |
a5eccbdc2b | ||
![]() |
d0318e3e83 | ||
![]() |
d1c308f118 | ||
![]() |
3871170e44 | ||
![]() |
95dd5c4a7c | ||
![]() |
0bff4b55a5 | ||
![]() |
a2022415c2 | ||
![]() |
2b8bd8144f | ||
![]() |
7bf520ac8c | ||
![]() |
ad8983e889 | ||
![]() |
d0b62af32e | ||
![]() |
bc8e259974 | ||
![]() |
ff0a4661fd | ||
![]() |
9151df6816 | ||
![]() |
9c0878408b | ||
![]() |
61baa53076 | ||
![]() |
b2841ee9ab | ||
![]() |
9edea17fb7 | ||
![]() |
ac17618f0c | ||
![]() |
e94ed4eafa | ||
![]() |
8c33a5e62f | ||
![]() |
f9f1017e5b | ||
![]() |
5d2d831b9e | ||
![]() |
562d9a0f4a | ||
![]() |
b981f9199b | ||
![]() |
efef0f3734 | ||
![]() |
cd0b860210 | ||
![]() |
9cb0655cfa | ||
![]() |
3775f28af7 | ||
![]() |
c33b824871 | ||
![]() |
cf396b739e | ||
![]() |
631963f43c | ||
![]() |
06cedb4f41 | ||
![]() |
7a0c60a164 | ||
![]() |
4c038ad932 | ||
![]() |
f6dd38685a | ||
![]() |
2eab0f0567 | ||
![]() |
8fedc0c605 | ||
![]() |
5831b06842 | ||
![]() |
57d5b5f386 | ||
![]() |
c0f6b1ed73 | ||
![]() |
f4cd1c30cd | ||
![]() |
6b717f560e | ||
![]() |
e8a3db76bb | ||
![]() |
5acf7f2953 | ||
![]() |
e760e9f600 | ||
![]() |
8a47972b10 | ||
![]() |
f8e0c9524e | ||
![]() |
919aeaaed5 | ||
![]() |
9474512cbd | ||
![]() |
8c50f6c6d6 | ||
![]() |
01ee7209c8 | ||
![]() |
ff7c4eae28 | ||
![]() |
9373d58b53 | ||
![]() |
df71ade69f | ||
![]() |
2ed29be072 | ||
![]() |
a398ab7d4b | ||
![]() |
a0125e7195 | ||
![]() |
cb9a1cfff6 | ||
![]() |
445e73521a | ||
![]() |
7bdc922d2c | ||
![]() |
0c40c8f269 | ||
![]() |
681ddb99a2 | ||
![]() |
5dff319746 | ||
![]() |
57c4a00ac6 | ||
![]() |
286a0a8678 | ||
![]() |
17eff7d77f | ||
![]() |
2e22369ce0 | ||
![]() |
15c59b440f | ||
![]() |
8fb015acbf | ||
![]() |
f7b11c90fd | ||
![]() |
df93a35062 | ||
![]() |
ecb19a8412 | ||
![]() |
b1e5b40800 | ||
![]() |
daa3a293f2 | ||
![]() |
907264fc67 | ||
![]() |
c6becb93dc | ||
![]() |
2954bd2f1a | ||
![]() |
35322d2c15 | ||
![]() |
9f6a7eb368 | ||
![]() |
fec81808e5 | ||
![]() |
4db890484d | ||
![]() |
dfd5e06889 | ||
![]() |
71da8e14da | ||
![]() |
7dc37e3e0d | ||
![]() |
3de058a078 | ||
![]() |
4d70c7adce | ||
![]() |
5b41106476 | ||
![]() |
6240b22e66 | ||
![]() |
0e26f5afd7 | ||
![]() |
114bc06dbb |
12
.dockerignore
Normal file
12
.dockerignore
Normal 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
2
.gitignore
vendored
@@ -4,3 +4,5 @@
|
|||||||
.gradle
|
.gradle
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
|
**/*.rej
|
||||||
|
**/*.orig
|
||||||
|
9
.tx/config
Normal file
9
.tx/config
Normal 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
64
Dockerfile
Normal 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=11
|
||||||
|
|
||||||
|
# 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"
|
42
README.md
42
README.md
@@ -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.
|
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
|
./gradlew clean assemble
|
||||||
@@ -19,27 +19,37 @@ If you want to run the unit tests, type
|
|||||||
./gradlew clean build
|
./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`
|
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`
|
[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.
|
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
|
||||||
|
|
||||||
|
MuWire is available as a Docker image. For more information see the [Docker] page.
|
||||||
|
|
||||||
|
## 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
|
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||||
@@ -49,3 +59,11 @@ You can find the full key at https://keybase.io/zlatinb
|
|||||||
|
|
||||||
|
|
||||||
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
[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
|
||||||
|
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||||
|
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||||
|
42
TODO.md
42
TODO.md
@@ -1,8 +1,6 @@
|
|||||||
# TODO List
|
# TODO List
|
||||||
|
|
||||||
Not in any particular order yet
|
### Network
|
||||||
|
|
||||||
### Big Items
|
|
||||||
|
|
||||||
##### Bloom Filters
|
##### Bloom Filters
|
||||||
|
|
||||||
@@ -12,15 +10,33 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
|||||||
|
|
||||||
This helps with scalability
|
This helps with scalability
|
||||||
|
|
||||||
##### Web UI, REST Interface, etc.
|
### Core
|
||||||
|
|
||||||
Basically any non-gui non-cli user interface
|
* Metadata parsing and search
|
||||||
|
|
||||||
##### 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
|
|
||||||
* Automatic adjustment of number of I2P tunnels
|
* 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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -2,8 +2,9 @@ subprojects {
|
|||||||
apply plugin: 'groovy'
|
apply plugin: 'groovy'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "net.i2p:i2p:${i2pVersion}"
|
compile 'org.codehaus.groovy:groovy:2.4.15'
|
||||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
compile 'org.codehaus.groovy:groovy-jsr223:2.4.15'
|
||||||
|
compile 'org.codehaus.groovy:groovy-json:2.4.15'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileGroovy {
|
compileGroovy {
|
||||||
|
@@ -72,4 +72,27 @@ class BrowseModel {
|
|||||||
void setPercentageLabel(Label percentage) {
|
void setPercentageLabel(Label percentage) {
|
||||||
this.percentage = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
|
|||||||
}
|
}
|
||||||
contentPanel.addComponent(table, layoutData)
|
contentPanel.addComponent(table, layoutData)
|
||||||
|
|
||||||
|
Panel buttonsPanel = new Panel()
|
||||||
|
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||||
|
Button sortButton = new Button("Sort...", {sort()})
|
||||||
Button closeButton = new Button("Close",{
|
Button closeButton = new Button("Close",{
|
||||||
model.unregister()
|
model.unregister()
|
||||||
close()
|
close()
|
||||||
})
|
})
|
||||||
contentPanel.addComponent(closeButton, layoutData)
|
buttonsPanel.addComponent(sortButton, layoutData)
|
||||||
|
buttonsPanel.addComponent(closeButton, layoutData)
|
||||||
|
|
||||||
|
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||||
setComponent(contentPanel)
|
setComponent(contentPanel)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -120,4 +126,11 @@ class BrowseView extends BasicWindow {
|
|||||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||||
textGUI.addWindowAndWait(view)
|
textGUI.addWindowAndWait(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sort() {
|
||||||
|
SortPrompt prompt = new SortPrompt(textGUI)
|
||||||
|
SortType type = prompt.prompt()
|
||||||
|
if (type != null)
|
||||||
|
model.sort(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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")})
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
|||||||
import com.muwire.core.files.AllFilesLoadedEvent
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
|
|
||||||
class CliLanterna {
|
class CliLanterna {
|
||||||
private static final String MW_VERSION = "0.6.4"
|
private static final String MW_VERSION = "0.6.9"
|
||||||
|
|
||||||
private static volatile Core core
|
private static volatile Core core
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ package com.muwire.clilanterna
|
|||||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||||
import com.googlecode.lanterna.gui2.table.TableModel
|
import com.googlecode.lanterna.gui2.table.TableModel
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.files.AllFilesLoadedEvent
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
import com.muwire.core.files.DirectoryWatchedEvent
|
import com.muwire.core.files.DirectoryWatchedEvent
|
||||||
@@ -72,10 +73,38 @@ class FilesModel {
|
|||||||
sharedFiles.each {
|
sharedFiles.each {
|
||||||
long size = it.getCachedLength()
|
long size = it.getCachedLength()
|
||||||
boolean comment = it.comment != null
|
boolean comment = it.comment != null
|
||||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
|
||||||
String hits = String.valueOf(it.getHits())
|
String hits = String.valueOf(it.getHits())
|
||||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
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()
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,6 @@ import com.muwire.core.filecert.UICreateCertificateEvent
|
|||||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
import com.muwire.core.files.FileUnsharedEvent
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.files.UIPersistFilesEvent
|
|
||||||
|
|
||||||
class FilesView extends BasicWindow {
|
class FilesView extends BasicWindow {
|
||||||
private final FilesModel model
|
private final FilesModel model
|
||||||
@@ -51,17 +50,19 @@ class FilesView extends BasicWindow {
|
|||||||
contentPanel.addComponent(table, layoutData)
|
contentPanel.addComponent(table, layoutData)
|
||||||
|
|
||||||
Panel buttonsPanel = new Panel()
|
Panel buttonsPanel = new Panel()
|
||||||
buttonsPanel.setLayoutManager(new GridLayout(4))
|
buttonsPanel.setLayoutManager(new GridLayout(5))
|
||||||
|
|
||||||
Button shareFile = new Button("Share File", {shareFile()})
|
Button shareFile = new Button("Share File", {shareFile()})
|
||||||
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
|
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
|
||||||
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
|
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
|
||||||
|
Button sort = new Button("Sort...",{sort()})
|
||||||
Button close = new Button("Close", {close()})
|
Button close = new Button("Close", {close()})
|
||||||
|
|
||||||
buttonsPanel.with {
|
buttonsPanel.with {
|
||||||
addComponent(shareFile, layoutData)
|
addComponent(shareFile, layoutData)
|
||||||
addComponent(shareDirectory, layoutData)
|
addComponent(shareDirectory, layoutData)
|
||||||
addComponent(unshareDirectory, layoutData)
|
addComponent(unshareDirectory, layoutData)
|
||||||
|
addComponent(sort, layoutData)
|
||||||
addComponent(close, layoutData)
|
addComponent(close, layoutData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,6 @@ class FilesView extends BasicWindow {
|
|||||||
|
|
||||||
Button unshareButton = new Button("Unshare", {
|
Button unshareButton = new Button("Unshare", {
|
||||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
core.eventBus.publish(new UIPersistFilesEvent())
|
|
||||||
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
||||||
} )
|
} )
|
||||||
Button addCommentButton = new Button("Add Comment", {
|
Button addCommentButton = new Button("Add Comment", {
|
||||||
@@ -134,4 +134,11 @@ class FilesView extends BasicWindow {
|
|||||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
|
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
|
||||||
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
|
|||||||
private final UploadsModel uploadsModel
|
private final UploadsModel uploadsModel
|
||||||
private final FilesModel filesModel
|
private final FilesModel filesModel
|
||||||
private final TrustModel trustModel
|
private final TrustModel trustModel
|
||||||
|
private final ChatConsoleModel chatModel
|
||||||
|
|
||||||
private final Label connectionCount, incoming, outgoing
|
private final Label connectionCount, incoming, outgoing
|
||||||
private final Label known, failing, hopeless
|
private final Label known, failing, hopeless
|
||||||
@@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
|
|||||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
||||||
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
||||||
trustModel = new TrustModel(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])
|
setHints([Window.Hint.EXPANDED])
|
||||||
Panel contentPanel = new Panel()
|
Panel contentPanel = new Panel()
|
||||||
@@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
|
|||||||
Panel buttonsPanel = new Panel()
|
Panel buttonsPanel = new Panel()
|
||||||
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
|
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
|
||||||
|
|
||||||
GridLayout gridLayout = new GridLayout(7)
|
GridLayout gridLayout = new GridLayout(8)
|
||||||
buttonsPanel.setLayoutManager(gridLayout)
|
buttonsPanel.setLayoutManager(gridLayout)
|
||||||
|
|
||||||
searchTextBox = new TextBox(new TerminalSize(40, 1))
|
searchTextBox = new TextBox(new TerminalSize(40, 1))
|
||||||
@@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
|
|||||||
Button uploadsButton = new Button("Uploads", {upload()})
|
Button uploadsButton = new Button("Uploads", {upload()})
|
||||||
Button filesButton = new Button("Files", { files() })
|
Button filesButton = new Button("Files", { files() })
|
||||||
Button trustButton = new Button("Trust", {trust()})
|
Button trustButton = new Button("Trust", {trust()})
|
||||||
|
Button chatButton = new Button("Chat", {chat()})
|
||||||
Button quitButton = new Button("Quit", {close()})
|
Button quitButton = new Button("Quit", {close()})
|
||||||
|
|
||||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||||
@@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
|
|||||||
addComponent(uploadsButton, layoutData)
|
addComponent(uploadsButton, layoutData)
|
||||||
addComponent(filesButton, layoutData)
|
addComponent(filesButton, layoutData)
|
||||||
addComponent(trustButton, layoutData)
|
addComponent(trustButton, layoutData)
|
||||||
|
addComponent(chatButton, layoutData)
|
||||||
addComponent(quitButton, layoutData)
|
addComponent(quitButton, layoutData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
|
|||||||
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
|
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void chat() {
|
||||||
|
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
|
||||||
|
}
|
||||||
|
|
||||||
private void refreshStats() {
|
private void refreshStats() {
|
||||||
int inCon = 0
|
int inCon = 0
|
||||||
int outCon = 0
|
int outCon = 0
|
||||||
|
@@ -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()
|
||||||
|
}
|
@@ -16,7 +16,27 @@ class ResultsModel {
|
|||||||
ResultsModel(UIResultBatchEvent results) {
|
ResultsModel(UIResultBatchEvent results) {
|
||||||
this.results = results
|
this.results = results
|
||||||
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
|
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 size = DataHelper.formatSize2Decimal(it.size, false) + "B"
|
||||||
String infoHash = Base64.encode(it.infohash.getRoot())
|
String infoHash = Base64.encode(it.infohash.getRoot())
|
||||||
String sources = String.valueOf(it.sources.size())
|
String sources = String.valueOf(it.sources.size())
|
||||||
|
@@ -43,9 +43,14 @@ class ResultsView extends BasicWindow {
|
|||||||
table.setTableModel(model.model)
|
table.setTableModel(model.model)
|
||||||
table.setVisibleRows(terminalSize.getRows())
|
table.setVisibleRows(terminalSize.getRows())
|
||||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
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()})
|
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)
|
setComponent(contentPanel)
|
||||||
closeButton.takeFocus()
|
closeButton.takeFocus()
|
||||||
@@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
|
|||||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||||
textGUI.addWindowAndWait(view)
|
textGUI.addWindowAndWait(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sort() {
|
||||||
|
SortPrompt prompt = new SortPrompt(textGUI)
|
||||||
|
SortType type = prompt.prompt()
|
||||||
|
if (type != null)
|
||||||
|
model.sort(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package com.muwire.clilanterna;
|
||||||
|
|
||||||
|
public enum SortType {
|
||||||
|
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
|
||||||
|
}
|
@@ -2,6 +2,7 @@ apply plugin : 'application'
|
|||||||
mainClassName = 'com.muwire.core.Core'
|
mainClassName = 'com.muwire.core.Core'
|
||||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compile "net.i2p:i2p:${i2pVersion}"
|
||||||
compile "net.i2p:router:${i2pVersion}"
|
compile "net.i2p:router:${i2pVersion}"
|
||||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
package com.muwire.core
|
package com.muwire.core
|
||||||
|
|
||||||
|
import com.muwire.core.files.PersisterDoneEvent
|
||||||
|
import com.muwire.core.files.PersisterFolderService
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@@ -31,7 +34,6 @@ import com.muwire.core.filecert.UIFetchCertificatesEvent
|
|||||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
import com.muwire.core.files.FileHashingEvent
|
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
import com.muwire.core.files.FileLoadedEvent
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
import com.muwire.core.files.FileManager
|
import com.muwire.core.files.FileManager
|
||||||
@@ -41,7 +43,7 @@ import com.muwire.core.files.HasherService
|
|||||||
import com.muwire.core.files.PersisterService
|
import com.muwire.core.files.PersisterService
|
||||||
import com.muwire.core.files.SideCarFileEvent
|
import com.muwire.core.files.SideCarFileEvent
|
||||||
import com.muwire.core.files.UICommentEvent
|
import com.muwire.core.files.UICommentEvent
|
||||||
import com.muwire.core.files.UIPersistFilesEvent
|
|
||||||
import com.muwire.core.files.AllFilesLoadedEvent
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
import com.muwire.core.files.DirectoryWatchedEvent
|
import com.muwire.core.files.DirectoryWatchedEvent
|
||||||
@@ -74,10 +76,8 @@ import net.i2p.client.I2PClientFactory
|
|||||||
import net.i2p.client.I2PSession
|
import net.i2p.client.I2PSession
|
||||||
import net.i2p.client.streaming.I2PSocketManager
|
import net.i2p.client.streaming.I2PSocketManager
|
||||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||||
import net.i2p.client.streaming.I2PSocketOptions
|
|
||||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||||
import net.i2p.crypto.DSAEngine
|
import net.i2p.crypto.DSAEngine
|
||||||
import net.i2p.crypto.SigType
|
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
import net.i2p.data.PrivateKey
|
import net.i2p.data.PrivateKey
|
||||||
import net.i2p.data.Signature
|
import net.i2p.data.Signature
|
||||||
@@ -91,13 +91,16 @@ public class Core {
|
|||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Persona me
|
final Persona me
|
||||||
|
final String version;
|
||||||
final File home
|
final File home
|
||||||
final Properties i2pOptions
|
final Properties i2pOptions
|
||||||
final MuWireSettings muOptions
|
final MuWireSettings muOptions
|
||||||
|
|
||||||
private final TrustService trustService
|
private final I2PSession i2pSession;
|
||||||
private final TrustSubscriber trustSubscriber
|
final TrustService trustService
|
||||||
|
final TrustSubscriber trustSubscriber
|
||||||
private final PersisterService persisterService
|
private final PersisterService persisterService
|
||||||
|
private final PersisterFolderService persisterFolderService
|
||||||
private final HostCache hostCache
|
private final HostCache hostCache
|
||||||
private final ConnectionManager connectionManager
|
private final ConnectionManager connectionManager
|
||||||
private final CacheClient cacheClient
|
private final CacheClient cacheClient
|
||||||
@@ -122,26 +125,27 @@ public class Core {
|
|||||||
|
|
||||||
public Core(MuWireSettings props, File home, String myVersion) {
|
public Core(MuWireSettings props, File home, String myVersion) {
|
||||||
this.home = home
|
this.home = home
|
||||||
|
this.version = myVersion
|
||||||
this.muOptions = props
|
this.muOptions = props
|
||||||
|
|
||||||
i2pOptions = new Properties()
|
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()) {
|
if (i2pOptionsFile.exists()) {
|
||||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||||
|
|
||||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
i2pOptions["inbound.nickname"] = "MuWire"
|
||||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||||
i2pOptions["outbound.nickname"] = "MuWire"
|
i2pOptions["outbound.nickname"] = "MuWire"
|
||||||
} else {
|
}
|
||||||
i2pOptions["inbound.nickname"] = "MuWire"
|
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
|
||||||
i2pOptions["outbound.nickname"] = "MuWire"
|
&& i2pOptions.hasProperty("i2np.udp.port")
|
||||||
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"
|
|
||||||
Random r = new Random()
|
Random r = new Random()
|
||||||
int port = r.nextInt(60000) + 4000
|
int port = r.nextInt(60000) + 4000
|
||||||
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||||
@@ -150,15 +154,18 @@ public class Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!props.embeddedRouter) {
|
if (!props.embeddedRouter) {
|
||||||
log.info "Initializing I2P context"
|
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
|
||||||
I2PAppContext.getGlobalContext().logManager()
|
log.info "Initializing I2P context"
|
||||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
I2PAppContext.getGlobalContext().logManager()
|
||||||
router = null
|
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||||
|
router = null
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("launching embedded router")
|
log.info("launching embedded router")
|
||||||
Properties routerProps = new Properties()
|
Properties routerProps = new Properties()
|
||||||
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
||||||
routerProps.setProperty("i2p.dir.config", 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("router.excludePeerCaps", "KLM")
|
||||||
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||||
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||||
@@ -185,7 +192,6 @@ public class Core {
|
|||||||
|
|
||||||
|
|
||||||
// options like tunnel length and quantity
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||||
@@ -254,7 +260,14 @@ public class Core {
|
|||||||
log.info "initializing persistence service"
|
log.info "initializing persistence service"
|
||||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||||
eventBus.register(UILoadedEvent.class, persisterService)
|
eventBus.register(UILoadedEvent.class, persisterService)
|
||||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
|
||||||
|
log.info "initializing folder persistence service"
|
||||||
|
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
|
||||||
|
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
|
||||||
|
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
|
||||||
|
eventBus.register(FileLoadedEvent.class, persisterFolderService)
|
||||||
|
eventBus.register(FileHashedEvent.class, persisterFolderService)
|
||||||
|
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
|
||||||
|
|
||||||
log.info("initializing host cache")
|
log.info("initializing host cache")
|
||||||
File hostStorage = new File(home, "hosts.json")
|
File hostStorage = new File(home, "hosts.json")
|
||||||
@@ -274,10 +287,13 @@ public class Core {
|
|||||||
log.info("initializing cache client")
|
log.info("initializing cache client")
|
||||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||||
|
|
||||||
|
if (!props.plugin) {
|
||||||
log.info("initializing update client")
|
log.info("initializing update client")
|
||||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||||
|
} else
|
||||||
|
log.info("running as plugin, not initializing update client")
|
||||||
|
|
||||||
log.info("initializing connector")
|
log.info("initializing connector")
|
||||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||||
@@ -313,7 +329,7 @@ public class Core {
|
|||||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
log.info("initializing upload manager")
|
||||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||||
|
|
||||||
log.info("initializing connection establisher")
|
log.info("initializing connection establisher")
|
||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
@@ -372,7 +388,7 @@ public class Core {
|
|||||||
connectionAcceptor.start()
|
connectionAcceptor.start()
|
||||||
connectionEstablisher.start()
|
connectionEstablisher.start()
|
||||||
hostCache.waitForLoad()
|
hostCache.waitForLoad()
|
||||||
updateClient.start()
|
updateClient?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
@@ -382,8 +398,16 @@ public class Core {
|
|||||||
}
|
}
|
||||||
log.info("saving settings")
|
log.info("saving settings")
|
||||||
saveMuSettings()
|
saveMuSettings()
|
||||||
|
log.info("shutting down host cache")
|
||||||
|
hostCache.stop()
|
||||||
log.info("shutting down trust subscriber")
|
log.info("shutting down trust subscriber")
|
||||||
trustSubscriber.stop()
|
trustSubscriber.stop()
|
||||||
|
log.info("shutting down trust service")
|
||||||
|
trustService.stop()
|
||||||
|
log.info("shutting down persister service")
|
||||||
|
persisterService.stop()
|
||||||
|
log.info("shutting down persisterFolder service")
|
||||||
|
persisterFolderService.stop()
|
||||||
log.info("shutting down download manager")
|
log.info("shutting down download manager")
|
||||||
downloadManager.shutdown()
|
downloadManager.shutdown()
|
||||||
log.info("shutting down connection acceptor")
|
log.info("shutting down connection acceptor")
|
||||||
@@ -400,10 +424,14 @@ public class Core {
|
|||||||
chatManager.shutdown()
|
chatManager.shutdown()
|
||||||
log.info("shutting down connection manager")
|
log.info("shutting down connection manager")
|
||||||
connectionManager.shutdown()
|
connectionManager.shutdown()
|
||||||
|
log.info("killing i2p session")
|
||||||
|
i2pSession.destroySession()
|
||||||
if (router != null) {
|
if (router != null) {
|
||||||
log.info("shutting down embedded router")
|
log.info("shutting down embedded router")
|
||||||
router.shutdown(0)
|
router.shutdown(0)
|
||||||
}
|
}
|
||||||
|
log.info("shutting down event bus");
|
||||||
|
eventBus.shutdown()
|
||||||
log.info("shutdown complete")
|
log.info("shutdown complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +439,11 @@ public class Core {
|
|||||||
File f = new File(home, "MuWire.properties")
|
File f = new File(home, "MuWire.properties")
|
||||||
f.withPrintWriter("UTF-8", { muOptions.write(it) })
|
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) {
|
static main(args) {
|
||||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||||
@@ -436,7 +469,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.6.4")
|
Core core = new Core(props, home, "0.6.9")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -2,6 +2,7 @@ package com.muwire.core
|
|||||||
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ import groovy.util.logging.Log
|
|||||||
class EventBus {
|
class EventBus {
|
||||||
|
|
||||||
private Map handlers = new HashMap()
|
private Map handlers = new HashMap()
|
||||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
|
||||||
def rv = new Thread(r)
|
def rv = new Thread(r)
|
||||||
rv.setDaemon(true)
|
rv.setDaemon(true)
|
||||||
rv.setName("event-bus")
|
rv.setName("event-bus")
|
||||||
@@ -53,4 +54,8 @@ class EventBus {
|
|||||||
log.info("Unregistering $handler for type $eventType")
|
log.info("Unregistering $handler for type $eventType")
|
||||||
handlers[eventType]?.remove(handler)
|
handlers[eventType]?.remove(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void shutdown() {
|
||||||
|
executor.shutdownNow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,12 +34,14 @@ class MuWireSettings {
|
|||||||
boolean startChatServer
|
boolean startChatServer
|
||||||
int maxChatConnections
|
int maxChatConnections
|
||||||
boolean advertiseChat
|
boolean advertiseChat
|
||||||
|
File chatWelcomeFile
|
||||||
Set<String> watchedDirectories
|
Set<String> watchedDirectories
|
||||||
float downloadSequentialRatio
|
float downloadSequentialRatio
|
||||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||||
int meshExpiration
|
int meshExpiration
|
||||||
int speedSmoothSeconds
|
int speedSmoothSeconds
|
||||||
boolean embeddedRouter
|
boolean embeddedRouter
|
||||||
|
boolean plugin
|
||||||
int inBw, outBw
|
int inBw, outBw
|
||||||
Set<String> watchedKeywords
|
Set<String> watchedKeywords
|
||||||
Set<String> watchedRegexes
|
Set<String> watchedRegexes
|
||||||
@@ -75,6 +77,7 @@ class MuWireSettings {
|
|||||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||||
|
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
|
||||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||||
@@ -85,6 +88,9 @@ class MuWireSettings {
|
|||||||
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
||||||
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
|
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
|
||||||
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
|
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
|
||||||
|
String chatWelcomeProp = props.getProperty("chatWelcomeFile")
|
||||||
|
if (chatWelcomeProp != null)
|
||||||
|
chatWelcomeFile = new File(chatWelcomeProp)
|
||||||
|
|
||||||
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
||||||
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
||||||
@@ -126,6 +132,7 @@ class MuWireSettings {
|
|||||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||||
|
props.setProperty("plugin", String.valueOf(plugin))
|
||||||
props.setProperty("inBw", String.valueOf(inBw))
|
props.setProperty("inBw", String.valueOf(inBw))
|
||||||
props.setProperty("outBw", String.valueOf(outBw))
|
props.setProperty("outBw", String.valueOf(outBw))
|
||||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||||
@@ -136,6 +143,8 @@ class MuWireSettings {
|
|||||||
props.setProperty("startChatServer", String.valueOf(startChatServer))
|
props.setProperty("startChatServer", String.valueOf(startChatServer))
|
||||||
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
|
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
|
||||||
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
|
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
|
||||||
|
if (chatWelcomeFile != null)
|
||||||
|
props.setProperty("chatWelcomeFile", chatWelcomeFile.getAbsolutePath())
|
||||||
|
|
||||||
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||||
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||||
|
@@ -1,20 +1,24 @@
|
|||||||
package com.muwire.core.chat;
|
package com.muwire.core.chat;
|
||||||
|
|
||||||
enum ChatAction {
|
enum ChatAction {
|
||||||
JOIN(true, false, true),
|
JOIN(true, false, true, false),
|
||||||
LEAVE(false, false, true),
|
LEAVE(false, false, true, false),
|
||||||
SAY(false, false, true),
|
SAY(false, false, true, false),
|
||||||
LIST(true, true, true),
|
LIST(true, true, true, false),
|
||||||
HELP(true, true, true),
|
HELP(true, true, true, false),
|
||||||
INFO(true, true, true),
|
INFO(true, true, true, false),
|
||||||
JOINED(true, true, false);
|
JOINED(true, true, false, false),
|
||||||
|
TRUST(true, false, true, true),
|
||||||
|
DISTRUST(true, false, true, true);
|
||||||
|
|
||||||
final boolean console;
|
final boolean console;
|
||||||
final boolean stateless;
|
final boolean stateless;
|
||||||
final boolean user;
|
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.console = console;
|
||||||
this.stateless = stateless;
|
this.stateless = stateless;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.local = local;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,10 +28,10 @@ class ChatClient implements Closeable {
|
|||||||
private final TrustService trustService
|
private final TrustService trustService
|
||||||
private final MuWireSettings settings
|
private final MuWireSettings settings
|
||||||
|
|
||||||
private volatile ChatConnection connection
|
private ChatConnection connection
|
||||||
private volatile boolean connectInProgress
|
private boolean connectInProgress
|
||||||
private volatile long lastRejectionTime
|
private long lastRejectionTime
|
||||||
private volatile Thread connectThread
|
private Thread connectThread
|
||||||
|
|
||||||
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
|
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
|
||||||
MuWireSettings settings) {
|
MuWireSettings settings) {
|
||||||
@@ -43,15 +43,19 @@ class ChatClient implements Closeable {
|
|||||||
this.settings = settings
|
this.settings = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
void connectIfNeeded() {
|
synchronized void connectIfNeeded() {
|
||||||
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
|
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
|
||||||
return
|
return
|
||||||
|
connectInProgress = true
|
||||||
CONNECTOR.execute({connect()})
|
CONNECTOR.execute({connect()})
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect() {
|
private void connect() {
|
||||||
connectInProgress = true
|
synchronized(this) {
|
||||||
connectThread = Thread.currentThread()
|
if (!connectInProgress)
|
||||||
|
return
|
||||||
|
connectThread = Thread.currentThread()
|
||||||
|
}
|
||||||
Endpoint endpoint = null
|
Endpoint endpoint = null
|
||||||
try {
|
try {
|
||||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
|
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
|
||||||
@@ -72,8 +76,11 @@ class ChatClient implements Closeable {
|
|||||||
|
|
||||||
if (code == 429) {
|
if (code == 429) {
|
||||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
|
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
|
||||||
|
try { dos.close() } catch (IOException ignore) {}
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
lastRejectionTime = System.currentTimeMillis()
|
synchronized(this) {
|
||||||
|
lastRejectionTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,32 +95,47 @@ class ChatClient implements Closeable {
|
|||||||
if (version != Constants.CHAT_VERSION)
|
if (version != Constants.CHAT_VERSION)
|
||||||
throw new Exception("Unknown chat version $version")
|
throw new Exception("Unknown chat version $version")
|
||||||
|
|
||||||
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
|
synchronized(this) {
|
||||||
connection.start()
|
if (!connectInProgress)
|
||||||
|
return
|
||||||
|
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
|
||||||
|
connection.start()
|
||||||
|
}
|
||||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
|
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
|
||||||
connection : connection))
|
connection : connection))
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.log(java.util.logging.Level.WARNING, "connect failed", e)
|
||||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
|
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
|
||||||
endpoint?.close()
|
if (endpoint != null) {
|
||||||
|
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
|
||||||
|
endpoint.close()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
connectInProgress = false
|
synchronized(this) {
|
||||||
connectThread = null
|
connectInProgress = false
|
||||||
|
connectThread = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnected() {
|
synchronized void disconnected() {
|
||||||
connectInProgress = false
|
connectInProgress = false
|
||||||
connection = null
|
connection = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
synchronized public void close() {
|
||||||
|
connectInProgress = false
|
||||||
connectThread?.interrupt()
|
connectThread?.interrupt()
|
||||||
connection?.close()
|
connection?.close()
|
||||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
|
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
|
||||||
}
|
}
|
||||||
|
|
||||||
void ping() {
|
synchronized void ping() {
|
||||||
connection?.sendPing()
|
connection?.sendPing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void sendChat(ChatMessageEvent e) {
|
||||||
|
connection?.sendChat(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -109,6 +109,7 @@ class ChatConnection implements ChatLink {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"unhandled exception in reader", e)
|
log.log(Level.WARNING,"unhandled exception in reader", e)
|
||||||
} finally {
|
} finally {
|
||||||
|
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +124,7 @@ class ChatConnection implements ChatLink {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.log(Level.WARNING,"unhandled exception in writer",e)
|
log.log(Level.WARNING,"unhandled exception in writer",e)
|
||||||
} finally {
|
} finally {
|
||||||
|
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,4 +7,8 @@ class ChatConnectionEvent extends Event {
|
|||||||
ChatConnectionAttemptStatus status
|
ChatConnectionAttemptStatus status
|
||||||
Persona persona
|
Persona persona
|
||||||
ChatLink connection
|
ChatLink connection
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ class ChatManager {
|
|||||||
return
|
return
|
||||||
if (e.sender != me)
|
if (e.sender != me)
|
||||||
return
|
return
|
||||||
clients[e.host]?.connection?.sendChat(e)
|
clients[e.host]?.sendChat(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||||
|
@@ -25,6 +25,8 @@ import net.i2p.util.ConcurrentHashSet
|
|||||||
@Log
|
@Log
|
||||||
class ChatServer {
|
class ChatServer {
|
||||||
public static final String CONSOLE = "__CONSOLE__"
|
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 EventBus eventBus
|
||||||
private final MuWireSettings settings
|
private final MuWireSettings settings
|
||||||
private final TrustService trustService
|
private final TrustService trustService
|
||||||
@@ -34,6 +36,7 @@ class ChatServer {
|
|||||||
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
|
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
|
||||||
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
|
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
|
||||||
private final Map<Persona, Set<String>> memberships = 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()
|
private final AtomicBoolean running = new AtomicBoolean()
|
||||||
|
|
||||||
@@ -49,10 +52,19 @@ class ChatServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
running.set(true)
|
if (!running.compareAndSet(false, true))
|
||||||
|
return
|
||||||
connections.put(me.destination, LocalChatLink.INSTANCE)
|
connections.put(me.destination, LocalChatLink.INSTANCE)
|
||||||
joinRoom(me, CONSOLE)
|
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() {
|
private void sendPings() {
|
||||||
@@ -105,8 +117,9 @@ class ChatServer {
|
|||||||
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
|
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
|
||||||
connections.put(endpoint.destination, connection)
|
connections.put(endpoint.destination, connection)
|
||||||
joinRoom(client, CONSOLE)
|
joinRoom(client, CONSOLE)
|
||||||
|
shortNames.put(client.getHumanReadableName(), client)
|
||||||
connection.start()
|
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) {
|
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||||
@@ -120,6 +133,7 @@ class ChatServer {
|
|||||||
leaveRoom(e.persona, it)
|
leaveRoom(e.persona, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
shortNames.remove(e.persona.getHumanReadableName())
|
||||||
connections.each { k, v ->
|
connections.each { k, v ->
|
||||||
v.sendLeave(e.persona)
|
v.sendLeave(e.persona)
|
||||||
}
|
}
|
||||||
@@ -131,7 +145,7 @@ class ChatServer {
|
|||||||
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
|
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
|
||||||
return
|
return
|
||||||
|
|
||||||
ChatConnection connection = connections.remove(e.persona.destination)
|
ChatConnection connection = connections.get(e.persona.destination)
|
||||||
connection?.close()
|
connection?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +201,9 @@ class ChatServer {
|
|||||||
(!command.action.console && e.room == CONSOLE) ||
|
(!command.action.console && e.room == CONSOLE) ||
|
||||||
!command.action.user)
|
!command.action.user)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (command.action.local && e.sender != me)
|
||||||
|
return
|
||||||
|
|
||||||
switch(command.action) {
|
switch(command.action) {
|
||||||
case ChatAction.JOIN : processJoin(command.payload, e); break
|
case ChatAction.JOIN : processJoin(command.payload, e); break
|
||||||
@@ -195,6 +212,8 @@ class ChatServer {
|
|||||||
case ChatAction.LIST : processList(e.sender.destination); break
|
case ChatAction.LIST : processList(e.sender.destination); break
|
||||||
case ChatAction.INFO : processInfo(e.sender.destination); break
|
case ChatAction.INFO : processInfo(e.sender.destination); break
|
||||||
case ChatAction.HELP : processHelp(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) {
|
private void processHelp(Destination d) {
|
||||||
String help = """/SAY
|
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
|
/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
|
/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
|
/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
|
/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
|
/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
|
/HELP - prints this help message
|
||||||
"""
|
"""
|
||||||
echo(help, d)
|
echo(help, d)
|
||||||
@@ -292,6 +313,13 @@ class ChatServer {
|
|||||||
connections[d]?.sendChat(echo)
|
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() {
|
void stop() {
|
||||||
if (running.compareAndSet(true, false)) {
|
if (running.compareAndSet(true, false)) {
|
||||||
connections.each { k, v ->
|
connections.each { k, v ->
|
||||||
|
@@ -255,7 +255,6 @@ abstract class Connection implements Closeable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this mandatory at some point
|
|
||||||
byte[] sig2 = null
|
byte[] sig2 = null
|
||||||
long queryTime = 0
|
long queryTime = 0
|
||||||
if (search.sig2 != null) {
|
if (search.sig2 != null) {
|
||||||
@@ -278,8 +277,10 @@ abstract class Connection implements Closeable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
log.info("no extended signature in query")
|
log.info("no extended signature in query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||||
searchHash : infohash,
|
searchHash : infohash,
|
||||||
|
@@ -369,13 +369,18 @@ class ConnectionAcceptor {
|
|||||||
|
|
||||||
def sharedFiles = fileManager.getSharedFiles().values()
|
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))
|
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||||
JsonOutput jsonOutput = new JsonOutput()
|
JsonOutput jsonOutput = new JsonOutput()
|
||||||
sharedFiles.each {
|
sharedFiles.each {
|
||||||
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
||||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
dos.writeShort((short)json.length())
|
dos.writeShort((short)json.length())
|
||||||
|
@@ -92,6 +92,22 @@ public class Downloader {
|
|||||||
public synchronized InfoHash getInfoHash() {
|
public synchronized InfoHash getInfoHash() {
|
||||||
infoHash
|
infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getFile() {
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNPieces() {
|
||||||
|
nPieces
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPieceSize() {
|
||||||
|
pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLength() {
|
||||||
|
length
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
@@ -249,6 +265,10 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
active
|
active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTotalWorkers() {
|
||||||
|
return activeWorkers.size();
|
||||||
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
paused = false
|
paused = false
|
||||||
@@ -385,8 +405,9 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
eventBus.publish(
|
eventBus.publish(
|
||||||
new FileDownloadedEvent(
|
new FileDownloadedEvent(
|
||||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
|
||||||
downloader : Downloader.this))
|
downloader : Downloader.this,
|
||||||
|
infoHash: getInfoHash()))
|
||||||
|
|
||||||
}
|
}
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
|
@@ -10,17 +10,20 @@ import net.i2p.crypto.DSAEngine
|
|||||||
import net.i2p.data.Signature
|
import net.i2p.data.Signature
|
||||||
import net.i2p.data.SigningPrivateKey
|
import net.i2p.data.SigningPrivateKey
|
||||||
import net.i2p.data.SigningPublicKey
|
import net.i2p.data.SigningPublicKey
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class Certificate {
|
class Certificate {
|
||||||
private final byte version
|
private final byte version
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final Name name, comment
|
final Name name, comment
|
||||||
private final long timestamp
|
final long timestamp
|
||||||
private final Persona issuer
|
final Persona issuer
|
||||||
private final byte[] sig
|
private final byte[] sig
|
||||||
|
|
||||||
private volatile byte [] payload
|
private volatile byte [] payload
|
||||||
|
|
||||||
|
private String base64;
|
||||||
|
|
||||||
Certificate(InputStream is) {
|
Certificate(InputStream is) {
|
||||||
version = (byte) (is.read() & 0xFF)
|
version = (byte) (is.read() & 0xFF)
|
||||||
if (version > Constants.FILE_CERT_VERSION)
|
if (version > Constants.FILE_CERT_VERSION)
|
||||||
@@ -131,6 +134,15 @@ class Certificate {
|
|||||||
os.write(payload)
|
os.write(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String toBase64() {
|
||||||
|
if (base64 == null) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||||
|
write(baos)
|
||||||
|
base64 = Base64.encode(baos.toByteArray())
|
||||||
|
}
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
|
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
|
||||||
|
@@ -32,7 +32,8 @@ class CertificateClient {
|
|||||||
fetcherThread.execute({
|
fetcherThread.execute({
|
||||||
Endpoint endpoint = null
|
Endpoint endpoint = null
|
||||||
try {
|
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)
|
endpoint = connector.connect(e.host.destination)
|
||||||
|
|
||||||
String infoHashString = Base64.encode(e.infoHash.getRoot())
|
String infoHashString = Base64.encode(e.infoHash.getRoot())
|
||||||
@@ -62,7 +63,8 @@ class CertificateClient {
|
|||||||
int count = Integer.parseInt(headers['Count'])
|
int count = Integer.parseInt(headers['Count'])
|
||||||
|
|
||||||
// start pulling the certs
|
// 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)
|
DataInputStream dis = new DataInputStream(is)
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
@@ -77,11 +79,14 @@ class CertificateClient {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (cert.infoHash == e.infoHash)
|
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) {
|
} catch (Exception bad) {
|
||||||
log.log(Level.WARNING,"Fetching certificates failed", 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 {
|
} finally {
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
package com.muwire.core.filecert
|
package com.muwire.core.filecert
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
class CertificateFetchEvent extends Event {
|
class CertificateFetchEvent extends Event {
|
||||||
CertificateFetchStatus status
|
CertificateFetchStatus status
|
||||||
int count
|
int count
|
||||||
|
Persona user
|
||||||
|
InfoHash infoHash
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
package com.muwire.core.filecert
|
package com.muwire.core.filecert
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
class CertificateFetchedEvent extends Event {
|
class CertificateFetchedEvent extends Event {
|
||||||
Certificate certificate
|
Certificate certificate
|
||||||
|
Persona user
|
||||||
|
InfoHash infoHash
|
||||||
}
|
}
|
||||||
|
@@ -70,7 +70,7 @@ class CertificateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
||||||
InfoHash infoHash = e.sharedFile.getInfoHash()
|
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
|
||||||
String name = e.sharedFile.getFile().getName()
|
String name = e.sharedFile.getFile().getName()
|
||||||
long timestamp = System.currentTimeMillis()
|
long timestamp = System.currentTimeMillis()
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ class CertificateManager {
|
|||||||
added
|
added
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasLocalCertificate(InfoHash infoHash) {
|
public boolean hasLocalCertificate(InfoHash infoHash) {
|
||||||
if (!byInfoHash.containsKey(infoHash))
|
if (!byInfoHash.containsKey(infoHash))
|
||||||
return false
|
return false
|
||||||
Set<Certificate> set = byInfoHash.get(infoHash)
|
Set<Certificate> set = byInfoHash.get(infoHash)
|
||||||
@@ -130,6 +130,13 @@ class CertificateManager {
|
|||||||
return false
|
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> getByInfoHash(InfoHash infoHash) {
|
||||||
Set<Certificate> rv = new HashSet<>()
|
Set<Certificate> rv = new HashSet<>()
|
||||||
if (byInfoHash.containsKey(infoHash))
|
if (byInfoHash.containsKey(infoHash))
|
||||||
|
@@ -0,0 +1,152 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.DownloadedFile
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.Service
|
||||||
|
import com.muwire.core.SharedFile
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
abstract class BasePersisterService extends Service{
|
||||||
|
|
||||||
|
protected static FileLoadedEvent fromJson(def json) {
|
||||||
|
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||||
|
throw new IllegalArgumentException()
|
||||||
|
if (!(json.hashList instanceof List))
|
||||||
|
throw new IllegalArgumentException()
|
||||||
|
|
||||||
|
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||||
|
file = file.getCanonicalFile()
|
||||||
|
if (!file.exists() || file.isDirectory())
|
||||||
|
return null
|
||||||
|
long length = Long.valueOf(json.length)
|
||||||
|
if (length != file.length())
|
||||||
|
return null
|
||||||
|
|
||||||
|
List hashList = (List) json.hashList
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||||
|
hashList.each {
|
||||||
|
byte [] hash = Base64.decode it.toString()
|
||||||
|
if (hash == null)
|
||||||
|
throw new IllegalArgumentException()
|
||||||
|
baos.write hash
|
||||||
|
}
|
||||||
|
byte[] hashListBytes = baos.toByteArray()
|
||||||
|
|
||||||
|
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||||
|
byte [] root = Base64.decode(json.infoHash.toString())
|
||||||
|
if (root == null)
|
||||||
|
throw new IllegalArgumentException()
|
||||||
|
if (!Arrays.equals(root, ih.getRoot()))
|
||||||
|
return null
|
||||||
|
|
||||||
|
int pieceSize = 0
|
||||||
|
if (json.pieceSize != null)
|
||||||
|
pieceSize = json.pieceSize
|
||||||
|
|
||||||
|
if (json.sources != null) {
|
||||||
|
List sources = (List)json.sources
|
||||||
|
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||||
|
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||||
|
df.setComment(json.comment)
|
||||||
|
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||||
|
sf.setComment(json.comment)
|
||||||
|
if (json.downloaders != null)
|
||||||
|
sf.getDownloaders().addAll(json.downloaders)
|
||||||
|
if (json.searchers != null) {
|
||||||
|
json.searchers.each {
|
||||||
|
Persona searcher = null
|
||||||
|
if (it.searcher != null)
|
||||||
|
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||||
|
long timestamp = it.timestamp
|
||||||
|
String query = it.query
|
||||||
|
sf.hit(searcher, timestamp, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static FileLoadedEvent fromJsonLite(json) {
|
||||||
|
if (json.file == null || json.length == null || json.root == null)
|
||||||
|
throw new IllegalArgumentException()
|
||||||
|
|
||||||
|
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||||
|
file = file.getCanonicalFile()
|
||||||
|
if (!file.exists() || file.isDirectory())
|
||||||
|
return null
|
||||||
|
long length = Long.valueOf(json.length)
|
||||||
|
if (length != file.length())
|
||||||
|
return null
|
||||||
|
|
||||||
|
byte[] root = Base64.decode(json.root)
|
||||||
|
InfoHash ih = new InfoHash(root)
|
||||||
|
|
||||||
|
int pieceSize = 0
|
||||||
|
if (json.pieceSize != null)
|
||||||
|
pieceSize = json.pieceSize
|
||||||
|
|
||||||
|
if (json.sources != null) {
|
||||||
|
List sources = (List)json.sources
|
||||||
|
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||||
|
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||||
|
df.setComment(json.comment)
|
||||||
|
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||||
|
sf.setComment(json.comment)
|
||||||
|
if (json.downloaders != null)
|
||||||
|
sf.getDownloaders().addAll(json.downloaders)
|
||||||
|
if (json.searchers != null) {
|
||||||
|
json.searchers.each {
|
||||||
|
Persona searcher = null
|
||||||
|
if (it.searcher != null)
|
||||||
|
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||||
|
long timestamp = it.timestamp
|
||||||
|
String query = it.query
|
||||||
|
sf.hit(searcher, timestamp, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static toJson(SharedFile sf) {
|
||||||
|
def json = [:]
|
||||||
|
json.file = sf.getB64EncodedFileName()
|
||||||
|
json.length = sf.getCachedLength()
|
||||||
|
json.root = Base64.encode(sf.getRoot())
|
||||||
|
json.pieceSize = sf.getPieceSize()
|
||||||
|
json.comment = sf.getComment()
|
||||||
|
json.hits = sf.getHits()
|
||||||
|
json.downloaders = sf.getDownloaders()
|
||||||
|
|
||||||
|
if (!sf.searches.isEmpty()) {
|
||||||
|
Set searchers = new HashSet<>()
|
||||||
|
sf.searches.each {
|
||||||
|
def search = [:]
|
||||||
|
if (it.searcher != null)
|
||||||
|
search.searcher = it.searcher.toBase64()
|
||||||
|
search.timestamp = it.timestamp
|
||||||
|
search.query = it.query
|
||||||
|
searchers.add(search)
|
||||||
|
}
|
||||||
|
json.searchers = searchers
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sf instanceof DownloadedFile) {
|
||||||
|
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
json
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ package com.muwire.core.files
|
|||||||
|
|
||||||
import com.muwire.core.DownloadedFile
|
import com.muwire.core.DownloadedFile
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
|
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -9,4 +10,5 @@ import net.i2p.data.Destination
|
|||||||
class FileDownloadedEvent extends Event {
|
class FileDownloadedEvent extends Event {
|
||||||
Downloader downloader
|
Downloader downloader
|
||||||
DownloadedFile downloadedFile
|
DownloadedFile downloadedFile
|
||||||
|
InfoHash infoHash
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
class FileHashedEvent extends Event {
|
class FileHashedEvent extends Event {
|
||||||
|
|
||||||
SharedFile sharedFile
|
SharedFile sharedFile
|
||||||
|
InfoHash infoHash
|
||||||
String error
|
String error
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -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);
|
||||||
|
}
|
@@ -1,9 +1,12 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
class FileLoadedEvent extends Event {
|
class FileLoadedEvent extends Event {
|
||||||
|
|
||||||
SharedFile loadedFile
|
SharedFile loadedFile
|
||||||
|
InfoHash infoHash
|
||||||
|
String source
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ class FileManager {
|
|||||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||||
final SearchIndex index = new SearchIndex()
|
final SearchIndex index = new SearchIndex()
|
||||||
final FileTree negativeTree = new FileTree()
|
final FileTree<Void> negativeTree = new FileTree<>()
|
||||||
final Set<File> sideCarFiles = new HashSet<>()
|
final Set<File> sideCarFiles = new HashSet<>()
|
||||||
|
|
||||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||||
@@ -32,7 +32,7 @@ class FileManager {
|
|||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
|
||||||
for (String negative : settings.negativeFileTree) {
|
for (String negative : settings.negativeFileTree) {
|
||||||
negativeTree.add(new File(negative))
|
negativeTree.add(new File(negative), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class FileManager {
|
|||||||
|
|
||||||
private void addToIndex(SharedFile sf) {
|
private void addToIndex(SharedFile sf) {
|
||||||
log.info("Adding shared file " + sf.getFile())
|
log.info("Adding shared file " + sf.getFile())
|
||||||
InfoHash infoHash = sf.getInfoHash()
|
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
log.info("adding new root")
|
log.info("adding new root")
|
||||||
@@ -88,7 +88,7 @@ class FileManager {
|
|||||||
negativeTree.remove(sf.file)
|
negativeTree.remove(sf.file)
|
||||||
String parent = sf.getFile().getParent()
|
String parent = sf.getFile().getParent()
|
||||||
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
||||||
negativeTree.add(sf.file.getParentFile())
|
negativeTree.add(sf.file.getParentFile(),null)
|
||||||
}
|
}
|
||||||
saveNegativeTree()
|
saveNegativeTree()
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ class FileManager {
|
|||||||
|
|
||||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||||
SharedFile sf = e.unsharedFile
|
SharedFile sf = e.unsharedFile
|
||||||
InfoHash infoHash = sf.getInfoHash()
|
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.remove(sf)
|
existing.remove(sf)
|
||||||
@@ -128,7 +128,7 @@ class FileManager {
|
|||||||
|
|
||||||
fileToSharedFile.remove(sf.file)
|
fileToSharedFile.remove(sf.file)
|
||||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||||
negativeTree.add(sf.file)
|
negativeTree.add(sf.file,null)
|
||||||
saveNegativeTree()
|
saveNegativeTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,12 +2,12 @@ package com.muwire.core.files
|
|||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class FileTree {
|
class FileTree<T> {
|
||||||
|
|
||||||
private final TreeNode root = new TreeNode()
|
private final TreeNode root = new TreeNode()
|
||||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
void add(File file) {
|
synchronized void add(File file, T value) {
|
||||||
List<File> path = new ArrayList<>()
|
List<File> path = new ArrayList<>()
|
||||||
path.add(file)
|
path.add(file)
|
||||||
while (file.getParentFile() != null) {
|
while (file.getParentFile() != null) {
|
||||||
@@ -29,9 +29,10 @@ class FileTree {
|
|||||||
}
|
}
|
||||||
current = existing
|
current = existing
|
||||||
}
|
}
|
||||||
|
current.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean remove(File file) {
|
synchronized boolean remove(File file) {
|
||||||
TreeNode node = fileToNode.remove(file)
|
TreeNode node = fileToNode.remove(file)
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return false
|
return false
|
||||||
@@ -45,13 +46,63 @@ class FileTree {
|
|||||||
true
|
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
|
TreeNode parent
|
||||||
File file
|
File file
|
||||||
|
T value;
|
||||||
final Set<TreeNode> children = new HashSet<>()
|
final Set<TreeNode> children = new HashSet<>()
|
||||||
|
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
file.hashCode()
|
Objects.hash(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
|
@@ -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);
|
||||||
|
}
|
@@ -65,7 +65,8 @@ class HasherService {
|
|||||||
} else {
|
} else {
|
||||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||||
def hash = hasher.hashFile f
|
def hash = hasher.hashFile f
|
||||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash.getRoot(), FileHasher.getPieceSize(f.length())),
|
||||||
|
infoHash : hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be triggered by the old PersisterService
|
||||||
|
* once it has finished reading the old file
|
||||||
|
*
|
||||||
|
* @see PersisterService
|
||||||
|
*/
|
||||||
|
class PersisterDoneEvent extends Event{
|
||||||
|
}
|
@@ -0,0 +1,174 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.*
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.ThreadFactory
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A persister that stores information about the files shared using
|
||||||
|
* individual JSON files in directories.
|
||||||
|
*
|
||||||
|
* The absolute path's 32bit hash to the shared file is used
|
||||||
|
* to build the directory and filename.
|
||||||
|
*
|
||||||
|
* This persister only starts working once the old persister has finished loading
|
||||||
|
* @see PersisterFolderService#getJsonPath
|
||||||
|
*/
|
||||||
|
@Log
|
||||||
|
class PersisterFolderService extends BasePersisterService {
|
||||||
|
|
||||||
|
final static int CUT_LENGTH = 6
|
||||||
|
|
||||||
|
private final Core core;
|
||||||
|
final File location
|
||||||
|
final EventBus listener
|
||||||
|
final int interval
|
||||||
|
final Timer timer
|
||||||
|
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||||
|
new Thread(r, "file persister")
|
||||||
|
} as ThreadFactory)
|
||||||
|
|
||||||
|
PersisterFolderService(Core core, File location, EventBus listener) {
|
||||||
|
this.core = core;
|
||||||
|
this.location = location
|
||||||
|
this.listener = listener
|
||||||
|
this.interval = interval
|
||||||
|
timer = new Timer("file-folder persister timer", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
timer.cancel()
|
||||||
|
persisterExecutor.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPersisterDoneEvent(PersisterDoneEvent persisterDoneEvent) {
|
||||||
|
log.info("Old persister done")
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileHashedEvent(FileHashedEvent hashedEvent) {
|
||||||
|
persistFile(hashedEvent.sharedFile, hashedEvent.infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileDownloadedEvent(FileDownloadedEvent downloadedEvent) {
|
||||||
|
if (core.getMuOptions().getShareDownloadedFiles()) {
|
||||||
|
persistFile(downloadedEvent.downloadedFile, downloadedEvent.infoHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rid of the json and hashlists of unshared files
|
||||||
|
* @param unsharedEvent
|
||||||
|
*/
|
||||||
|
void onFileUnsharedEvent(FileUnsharedEvent unsharedEvent) {
|
||||||
|
def jsonPath = getJsonPath(unsharedEvent.unsharedFile)
|
||||||
|
def jsonFile = jsonPath.toFile()
|
||||||
|
if(jsonFile.isFile()){
|
||||||
|
jsonFile.delete()
|
||||||
|
}
|
||||||
|
def hashListPath = getHashListPath(unsharedEvent.unsharedFile)
|
||||||
|
def hashListFile = hashListPath.toFile()
|
||||||
|
if (hashListFile.isFile())
|
||||||
|
hashListFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileLoadedEvent(FileLoadedEvent loadedEvent) {
|
||||||
|
if(loadedEvent.source == "PersisterService"){
|
||||||
|
log.info("Migrating persisted file from PersisterService: "
|
||||||
|
+ loadedEvent.loadedFile.file.absolutePath.toString())
|
||||||
|
persistFile(loadedEvent.loadedFile, loadedEvent.infoHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() {
|
||||||
|
log.fine("Loading...")
|
||||||
|
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||||
|
|
||||||
|
if (location.exists() && location.isDirectory()) {
|
||||||
|
try {
|
||||||
|
_load()
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
log.log(Level.WARNING, "couldn't load files", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
location.mkdirs()
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads every JSON into memory
|
||||||
|
*/
|
||||||
|
private void _load() {
|
||||||
|
int loaded = 0
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
Files.walk(location.toPath())
|
||||||
|
.filter({
|
||||||
|
it.getFileName().toString().endsWith(".json")
|
||||||
|
})
|
||||||
|
.forEach({
|
||||||
|
def parsed = slurper.parse it.toFile()
|
||||||
|
def event = fromJsonLite parsed
|
||||||
|
if (event == null) return
|
||||||
|
|
||||||
|
log.fine("loaded file $event.loadedFile.file")
|
||||||
|
listener.publish event
|
||||||
|
loaded++
|
||||||
|
if (loaded % 10 == 0)
|
||||||
|
Thread.sleep(20)
|
||||||
|
|
||||||
|
})
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistFile(SharedFile sf, InfoHash ih) {
|
||||||
|
persisterExecutor.submit({
|
||||||
|
def jsonPath = getJsonPath(sf)
|
||||||
|
|
||||||
|
def startTime = System.currentTimeMillis()
|
||||||
|
jsonPath.parent.toFile().mkdirs()
|
||||||
|
jsonPath.toFile().withPrintWriter { writer ->
|
||||||
|
def json = toJson sf
|
||||||
|
json = JsonOutput.toJson(json)
|
||||||
|
writer.println json
|
||||||
|
}
|
||||||
|
|
||||||
|
def hashListPath = getHashListPath(sf)
|
||||||
|
hashListPath.toFile().bytes = ih.hashList
|
||||||
|
log.fine("Time(ms) to write json+hashList: " + (System.currentTimeMillis() - startTime))
|
||||||
|
} as Runnable)
|
||||||
|
}
|
||||||
|
private Path getJsonPath(SharedFile sf){
|
||||||
|
def pathHash = sf.getB64PathHash()
|
||||||
|
return Paths.get(
|
||||||
|
location.getAbsolutePath(),
|
||||||
|
pathHash.substring(0, CUT_LENGTH),
|
||||||
|
pathHash.substring(CUT_LENGTH) + ".json"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getHashListPath(SharedFile sf) {
|
||||||
|
def pathHash = sf.getB64PathHash()
|
||||||
|
return Paths.get(
|
||||||
|
location.getAbsolutePath(),
|
||||||
|
pathHash.substring(0, CUT_LENGTH),
|
||||||
|
pathHash.substring(CUT_LENGTH) + ".hashlist"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash loadInfoHash(SharedFile sf) {
|
||||||
|
def path = getHashListPath(sf)
|
||||||
|
InfoHash.fromHashList(path.toFile().bytes)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,40 +1,24 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
import java.nio.file.CopyOption
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ThreadFactory
|
import java.util.concurrent.ThreadFactory
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
import com.muwire.core.DownloadedFile
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
|
||||||
import com.muwire.core.Persona
|
|
||||||
import com.muwire.core.Service
|
|
||||||
import com.muwire.core.SharedFile
|
|
||||||
import com.muwire.core.UILoadedEvent
|
import com.muwire.core.UILoadedEvent
|
||||||
import com.muwire.core.util.DataUtil
|
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
|
||||||
import net.i2p.data.Destination
|
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class PersisterService extends Service {
|
class PersisterService extends BasePersisterService {
|
||||||
|
|
||||||
final File location
|
final File location
|
||||||
final EventBus listener
|
final EventBus listener
|
||||||
final int interval
|
final int interval
|
||||||
final Timer timer
|
final Timer timer
|
||||||
final FileManager fileManager
|
final FileManager fileManager
|
||||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
|
||||||
new Thread(r, "file persister")
|
|
||||||
} as ThreadFactory)
|
|
||||||
|
|
||||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||||
this.location = location
|
this.location = location
|
||||||
@@ -51,10 +35,6 @@ class PersisterService extends Service {
|
|||||||
void onUILoadedEvent(UILoadedEvent e) {
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
timer.schedule({load()} as TimerTask, 1)
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
|
||||||
persistFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||||
@@ -69,6 +49,7 @@ class PersisterService extends Service {
|
|||||||
def event = fromJson parsed
|
def event = fromJson parsed
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
log.fine("loaded file $event.loadedFile.file")
|
log.fine("loaded file $event.loadedFile.file")
|
||||||
|
event.source = "PersisterService"
|
||||||
listener.publish event
|
listener.publish event
|
||||||
loaded++
|
loaded++
|
||||||
if (loaded % 10 == 0)
|
if (loaded % 10 == 0)
|
||||||
@@ -76,126 +57,18 @@ class PersisterService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listener.publish(new AllFilesLoadedEvent())
|
// Backup the old hashes
|
||||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
location.renameTo(
|
||||||
|
new File(location.absolutePath + ".bak")
|
||||||
|
)
|
||||||
|
listener.publish(new PersisterDoneEvent())
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
log.log(Level.WARNING, "couldn't load files",e)
|
log.log(Level.WARNING, "couldn't load files",e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
listener.publish(new AllFilesLoadedEvent())
|
listener.publish(new PersisterDoneEvent())
|
||||||
}
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FileLoadedEvent fromJson(def json) {
|
|
||||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
|
||||||
throw new IllegalArgumentException()
|
|
||||||
if (!(json.hashList instanceof List))
|
|
||||||
throw new IllegalArgumentException()
|
|
||||||
|
|
||||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
|
||||||
file = file.getCanonicalFile()
|
|
||||||
if (!file.exists() || file.isDirectory())
|
|
||||||
return null
|
|
||||||
long length = Long.valueOf(json.length)
|
|
||||||
if (length != file.length())
|
|
||||||
return null
|
|
||||||
|
|
||||||
List hashList = (List) json.hashList
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
|
||||||
hashList.each {
|
|
||||||
byte [] hash = Base64.decode it.toString()
|
|
||||||
if (hash == null)
|
|
||||||
throw new IllegalArgumentException()
|
|
||||||
baos.write hash
|
|
||||||
}
|
|
||||||
byte[] hashListBytes = baos.toByteArray()
|
|
||||||
|
|
||||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
|
||||||
byte [] root = Base64.decode(json.infoHash.toString())
|
|
||||||
if (root == null)
|
|
||||||
throw new IllegalArgumentException()
|
|
||||||
if (!Arrays.equals(root, ih.getRoot()))
|
|
||||||
return null
|
|
||||||
|
|
||||||
int pieceSize = 0
|
|
||||||
if (json.pieceSize != null)
|
|
||||||
pieceSize = json.pieceSize
|
|
||||||
|
|
||||||
if (json.sources != null) {
|
|
||||||
List sources = (List)json.sources
|
|
||||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
|
||||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
|
||||||
df.setComment(json.comment)
|
|
||||||
return new FileLoadedEvent(loadedFile : df)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
|
||||||
sf.setComment(json.comment)
|
|
||||||
if (json.downloaders != null)
|
|
||||||
sf.getDownloaders().addAll(json.downloaders)
|
|
||||||
if (json.searchers != null) {
|
|
||||||
json.searchers.each {
|
|
||||||
Persona searcher = null
|
|
||||||
if (it.searcher != null)
|
|
||||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
|
||||||
long timestamp = it.timestamp
|
|
||||||
String query = it.query
|
|
||||||
sf.hit(searcher, timestamp, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new FileLoadedEvent(loadedFile: sf)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void persistFiles() {
|
|
||||||
persisterExecutor.submit( {
|
|
||||||
def sharedFiles = fileManager.getSharedFiles()
|
|
||||||
|
|
||||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
|
||||||
tmp.deleteOnExit()
|
|
||||||
tmp.withPrintWriter { writer ->
|
|
||||||
sharedFiles.each { k, v ->
|
|
||||||
def json = toJson(k,v)
|
|
||||||
json = JsonOutput.toJson(json)
|
|
||||||
writer.println json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
tmp.delete()
|
|
||||||
} as Runnable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def toJson(File f, SharedFile sf) {
|
|
||||||
def json = [:]
|
|
||||||
json.file = sf.getB64EncodedFileName()
|
|
||||||
json.length = sf.getCachedLength()
|
|
||||||
InfoHash ih = sf.getInfoHash()
|
|
||||||
json.infoHash = sf.getB64EncodedHashRoot()
|
|
||||||
json.pieceSize = sf.getPieceSize()
|
|
||||||
json.hashList = sf.getB64EncodedHashList()
|
|
||||||
json.comment = sf.getComment()
|
|
||||||
json.hits = sf.getHits()
|
|
||||||
json.downloaders = sf.getDownloaders()
|
|
||||||
|
|
||||||
if (!sf.searches.isEmpty()) {
|
|
||||||
Set searchers = new HashSet<>()
|
|
||||||
sf.searches.each {
|
|
||||||
def search = [:]
|
|
||||||
if (it.searcher != null)
|
|
||||||
search.searcher = it.searcher.toBase64()
|
|
||||||
search.timestamp = it.timestamp
|
|
||||||
search.query = it.query
|
|
||||||
searchers.add(search)
|
|
||||||
}
|
|
||||||
json.searchers = searchers
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sf instanceof DownloadedFile) {
|
|
||||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
json
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
package com.muwire.core.files
|
|
||||||
|
|
||||||
import com.muwire.core.Event
|
|
||||||
|
|
||||||
class UIPersistFilesEvent extends Event {
|
|
||||||
}
|
|
@@ -1,5 +1,7 @@
|
|||||||
package com.muwire.core.hostcache
|
package com.muwire.core.hostcache
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.connection.ConnectionManager
|
import com.muwire.core.connection.ConnectionManager
|
||||||
@@ -27,6 +29,7 @@ class CacheClient {
|
|||||||
final long interval
|
final long interval
|
||||||
final MuWireSettings settings
|
final MuWireSettings settings
|
||||||
final Timer timer
|
final Timer timer
|
||||||
|
private final AtomicBoolean stopped = new AtomicBoolean();
|
||||||
|
|
||||||
public CacheClient(EventBus eventBus, HostCache cache,
|
public CacheClient(EventBus eventBus, HostCache cache,
|
||||||
ConnectionManager manager, I2PSession session,
|
ConnectionManager manager, I2PSession session,
|
||||||
@@ -47,9 +50,12 @@ class CacheClient {
|
|||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
|
stopped.set(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void queryIfNeeded() {
|
private void queryIfNeeded() {
|
||||||
|
if (stopped.get())
|
||||||
|
return
|
||||||
if (!manager.getConnections().isEmpty())
|
if (!manager.getConnections().isEmpty())
|
||||||
return
|
return
|
||||||
if (!cache.getHosts(1).isEmpty())
|
if (!cache.getHosts(1).isEmpty())
|
||||||
@@ -65,7 +71,12 @@ class CacheClient {
|
|||||||
options.setSendLeaseSet(true)
|
options.setSendLeaseSet(true)
|
||||||
CacheServers.getCacheServers().each {
|
CacheServers.getCacheServers().each {
|
||||||
log.info "Querying hostcache ${it.toBase32()}"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ class BrowseManager {
|
|||||||
browserThread.execute({
|
browserThread.execute({
|
||||||
Endpoint endpoint = null
|
Endpoint endpoint = null
|
||||||
try {
|
try {
|
||||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
|
eventBus.publish(new BrowseStatusEvent(host : e.host, status : BrowseStatus.CONNECTING))
|
||||||
endpoint = connector.connect(e.host.destination)
|
endpoint = connector.connect(e.host.destination)
|
||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
@@ -55,8 +55,10 @@ class BrowseManager {
|
|||||||
|
|
||||||
int results = Integer.parseInt(headers['Count'])
|
int results = Integer.parseInt(headers['Count'])
|
||||||
|
|
||||||
|
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
|
||||||
|
|
||||||
// at this stage, start pulling the results
|
// 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()
|
JsonSlurper slurper = new JsonSlurper()
|
||||||
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
||||||
@@ -67,14 +69,15 @@ class BrowseManager {
|
|||||||
dis.readFully(tmp)
|
dis.readFully(tmp)
|
||||||
def json = slurper.parse(tmp)
|
def json = slurper.parse(tmp)
|
||||||
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
|
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
|
||||||
|
result.chat = chat
|
||||||
eventBus.publish(result)
|
eventBus.publish(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
|
eventBus.publish(new BrowseStatusEvent(host: e.host, status : BrowseStatus.FINISHED))
|
||||||
|
|
||||||
} catch (Exception bad) {
|
} catch (Exception bad) {
|
||||||
log.log(Level.WARNING, "browse failed", 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 {
|
} finally {
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
class BrowseStatusEvent extends Event {
|
class BrowseStatusEvent extends Event {
|
||||||
|
Persona host
|
||||||
BrowseStatus status
|
BrowseStatus status
|
||||||
int totalResults
|
int totalResults
|
||||||
}
|
}
|
||||||
|
@@ -77,11 +77,11 @@ class ResultsSender {
|
|||||||
if (it.getComment() != null) {
|
if (it.getComment() != null) {
|
||||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||||
}
|
}
|
||||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||||
def uiResultEvent = new UIResultEvent( sender : me,
|
def uiResultEvent = new UIResultEvent( sender : me,
|
||||||
name : it.getFile().getName(),
|
name : it.getFile().getName(),
|
||||||
size : length,
|
size : length,
|
||||||
infohash : it.getInfoHash(),
|
infohash : new InfoHash(it.getRoot()),
|
||||||
pieceSize : pieceSize,
|
pieceSize : pieceSize,
|
||||||
uuid : uuid,
|
uuid : uuid,
|
||||||
browse : settings.browseFiles,
|
browse : settings.browseFiles,
|
||||||
@@ -119,7 +119,7 @@ class ResultsSender {
|
|||||||
me.write(os)
|
me.write(os)
|
||||||
os.writeShort((short)results.length)
|
os.writeShort((short)results.length)
|
||||||
results.each {
|
results.each {
|
||||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
os.writeShort((short)json.length())
|
os.writeShort((short)json.length())
|
||||||
@@ -141,7 +141,7 @@ class ResultsSender {
|
|||||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||||
results.each {
|
results.each {
|
||||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
dos.writeShort((short)json.length())
|
dos.writeShort((short)json.length())
|
||||||
@@ -170,7 +170,7 @@ class ResultsSender {
|
|||||||
obj.type = "Result"
|
obj.type = "Result"
|
||||||
obj.version = 2
|
obj.version = 2
|
||||||
obj.name = encodedName
|
obj.name = encodedName
|
||||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
obj.infohash = Base64.encode(sf.getRoot())
|
||||||
obj.size = sf.getCachedLength()
|
obj.size = sf.getCachedLength()
|
||||||
obj.pieceSize = sf.getPieceSize()
|
obj.pieceSize = sf.getPieceSize()
|
||||||
|
|
||||||
|
@@ -10,8 +10,8 @@ import net.i2p.util.ConcurrentHashSet
|
|||||||
class RemoteTrustList {
|
class RemoteTrustList {
|
||||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||||
|
|
||||||
private final Persona persona
|
final Persona persona
|
||||||
private final Set<TrustEntry> good, bad
|
final Set<TrustEntry> good, bad
|
||||||
volatile long timestamp
|
volatile long timestamp
|
||||||
volatile boolean forceUpdate
|
volatile boolean forceUpdate
|
||||||
Status status = Status.NEW
|
Status status = Status.NEW
|
||||||
|
@@ -130,8 +130,8 @@ class TrustService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class TrustEntry {
|
public static class TrustEntry {
|
||||||
private final Persona persona
|
final Persona persona
|
||||||
private final String reason
|
final String reason
|
||||||
TrustEntry(Persona persona, String reason) {
|
TrustEntry(Persona persona, String reason) {
|
||||||
this.persona = persona
|
this.persona = persona
|
||||||
this.reason = reason
|
this.reason = reason
|
||||||
|
@@ -26,7 +26,7 @@ class TrustSubscriber {
|
|||||||
private final I2PConnector i2pConnector
|
private final I2PConnector i2pConnector
|
||||||
private final MuWireSettings settings
|
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 final Object waitLock = new Object()
|
||||||
private volatile boolean shutdown
|
private volatile boolean shutdown
|
||||||
@@ -50,7 +50,7 @@ class TrustSubscriber {
|
|||||||
thread?.interrupt()
|
thread?.interrupt()
|
||||||
updateThreads.shutdownNow()
|
updateThreads.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
||||||
if (!e.subscribe) {
|
if (!e.subscribe) {
|
||||||
remoteTrustLists.remove(e.persona.destination)
|
remoteTrustLists.remove(e.persona.destination)
|
||||||
@@ -62,6 +62,10 @@ class TrustSubscriber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSubscribed(Persona p) {
|
||||||
|
remoteTrustLists.containsKey(p.destination)
|
||||||
|
}
|
||||||
|
|
||||||
private void checkLoop() {
|
private void checkLoop() {
|
||||||
try {
|
try {
|
||||||
|
@@ -83,7 +83,7 @@ class UpdateClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
if (e.downloadedFile.infoHash != updateInfoHash)
|
if (e.infoHash != updateInfoHash)
|
||||||
return
|
return
|
||||||
updateDownloading = false
|
updateDownloading = false
|
||||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
||||||
|
@@ -12,6 +12,7 @@ import com.muwire.core.download.DownloadManager
|
|||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
import com.muwire.core.download.SourceDiscoveredEvent
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
import com.muwire.core.files.FileManager
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.files.PersisterFolderService
|
||||||
import com.muwire.core.mesh.Mesh
|
import com.muwire.core.mesh.Mesh
|
||||||
import com.muwire.core.mesh.MeshManager
|
import com.muwire.core.mesh.MeshManager
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import net.i2p.data.Base64
|
|||||||
public class UploadManager {
|
public class UploadManager {
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
private final FileManager fileManager
|
private final FileManager fileManager
|
||||||
|
private final PersisterFolderService persisterService
|
||||||
private final MeshManager meshManager
|
private final MeshManager meshManager
|
||||||
private final DownloadManager downloadManager
|
private final DownloadManager downloadManager
|
||||||
private final MuWireSettings props
|
private final MuWireSettings props
|
||||||
@@ -34,9 +36,11 @@ public class UploadManager {
|
|||||||
|
|
||||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||||
MeshManager meshManager, DownloadManager downloadManager,
|
MeshManager meshManager, DownloadManager downloadManager,
|
||||||
|
PersisterFolderService persisterService,
|
||||||
MuWireSettings props) {
|
MuWireSettings props) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
|
this.persisterService = persisterService
|
||||||
this.meshManager = meshManager
|
this.meshManager = meshManager
|
||||||
this.downloadManager = downloadManager
|
this.downloadManager = downloadManager
|
||||||
this.props = props
|
this.props = props
|
||||||
@@ -162,7 +166,7 @@ public class UploadManager {
|
|||||||
|
|
||||||
InfoHash fullInfoHash
|
InfoHash fullInfoHash
|
||||||
if (downloader == null) {
|
if (downloader == null) {
|
||||||
fullInfoHash = sharedFiles.iterator().next().infoHash
|
fullInfoHash = persisterService.loadInfoHash(sharedFiles.iterator().next())
|
||||||
} else {
|
} else {
|
||||||
byte [] hashList = downloader.getInfoHash().getHashList()
|
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||||
if (hashList != null && hashList.length > 0)
|
if (hashList != null && hashList.length > 0)
|
||||||
|
@@ -11,6 +11,7 @@ public class Constants {
|
|||||||
|
|
||||||
public static final int MAX_HEADER_SIZE = 0x1 << 14;
|
public static final int MAX_HEADER_SIZE = 0x1 << 14;
|
||||||
public static final int MAX_HEADERS = 16;
|
public static final int MAX_HEADERS = 16;
|
||||||
|
public static final long MAX_HEADER_TIME = 60 * 1000;
|
||||||
|
|
||||||
public static final int MAX_RESULTS = 0x1 << 16;
|
public static final int MAX_RESULTS = 0x1 << 16;
|
||||||
|
|
||||||
|
@@ -10,9 +10,9 @@ public class DownloadedFile extends SharedFile {
|
|||||||
|
|
||||||
private final Set<Destination> sources;
|
private final Set<Destination> sources;
|
||||||
|
|
||||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
public DownloadedFile(File file, byte[] root, int pieceSize, Set<Destination> sources)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
super(file, infoHash, pieceSize);
|
super(file, root, pieceSize);
|
||||||
this.sources = sources;
|
this.sources = sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -68,11 +68,19 @@ public class Persona {
|
|||||||
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32);
|
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32);
|
||||||
return humanReadableName;
|
return humanReadableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Destination getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
public String toBase64() throws DataFormatException, IOException {
|
public String toBase64() {
|
||||||
if (base64 == null) {
|
if (base64 == null) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
write(baos);
|
try {
|
||||||
|
write(baos);
|
||||||
|
} catch (Exception impossible) {
|
||||||
|
throw new RuntimeException(impossible);
|
||||||
|
}
|
||||||
base64 = Base64.encode(baos.toByteArray());
|
base64 = Base64.encode(baos.toByteArray());
|
||||||
}
|
}
|
||||||
return base64;
|
return base64;
|
||||||
|
@@ -2,7 +2,10 @@ package com.muwire.core;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -16,44 +19,47 @@ import net.i2p.data.Base64;
|
|||||||
public class SharedFile {
|
public class SharedFile {
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
private final InfoHash infoHash;
|
private final byte[] root;
|
||||||
private final int pieceSize;
|
private final int pieceSize;
|
||||||
|
|
||||||
private final String cachedPath;
|
private final String cachedPath;
|
||||||
private final long cachedLength;
|
private final long cachedLength;
|
||||||
|
|
||||||
|
private String b64PathHash;
|
||||||
private final String b64EncodedFileName;
|
private final String b64EncodedFileName;
|
||||||
private final String b64EncodedHashRoot;
|
|
||||||
private final List<String> b64EncodedHashList;
|
|
||||||
|
|
||||||
private volatile String comment;
|
private volatile String comment;
|
||||||
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
|
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
|
||||||
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
|
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
public SharedFile(File file, byte[] root, int pieceSize) throws IOException {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.infoHash = infoHash;
|
this.root = root;
|
||||||
this.pieceSize = pieceSize;
|
this.pieceSize = pieceSize;
|
||||||
this.cachedPath = file.getAbsolutePath();
|
this.cachedPath = file.getAbsolutePath();
|
||||||
this.cachedLength = file.length();
|
this.cachedLength = file.length();
|
||||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
|
||||||
|
|
||||||
List<String> b64List = new ArrayList<String>();
|
|
||||||
byte[] tmp = new byte[32];
|
|
||||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
|
||||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
|
||||||
b64List.add(Base64.encode(tmp));
|
|
||||||
}
|
|
||||||
this.b64EncodedHashList = b64List;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
public File getFile() {
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InfoHash getInfoHash() {
|
public byte[] getPathHash() throws NoSuchAlgorithmException {
|
||||||
return infoHash;
|
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
||||||
|
digester.update(file.getAbsolutePath().getBytes());
|
||||||
|
return digester.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getB64PathHash() throws NoSuchAlgorithmException {
|
||||||
|
if(b64PathHash == null){
|
||||||
|
b64PathHash = Base64.encode(getPathHash());
|
||||||
|
}
|
||||||
|
return b64PathHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getRoot() {
|
||||||
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPieceSize() {
|
public int getPieceSize() {
|
||||||
@@ -73,14 +79,6 @@ public class SharedFile {
|
|||||||
return b64EncodedFileName;
|
return b64EncodedFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getB64EncodedHashRoot() {
|
|
||||||
return b64EncodedHashRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getB64EncodedHashList() {
|
|
||||||
return b64EncodedHashList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCachedPath() {
|
public String getCachedPath() {
|
||||||
return cachedPath;
|
return cachedPath;
|
||||||
}
|
}
|
||||||
@@ -119,7 +117,7 @@ public class SharedFile {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return file.hashCode() ^ infoHash.hashCode();
|
return file.hashCode() ^ Arrays.hashCode(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -127,7 +125,7 @@ public class SharedFile {
|
|||||||
if (!(o instanceof SharedFile))
|
if (!(o instanceof SharedFile))
|
||||||
return false;
|
return false;
|
||||||
SharedFile other = (SharedFile)o;
|
SharedFile other = (SharedFile)o;
|
||||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
return file.equals(other.file) && Arrays.equals(root, other.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SearchEntry {
|
public static class SearchEntry {
|
||||||
|
@@ -63,7 +63,7 @@ public class DataUtil {
|
|||||||
((int)header[2] & 0xFF);
|
((int)header[2] & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String readi18nString(byte [] encoded) {
|
public static String readi18nString(byte [] encoded) {
|
||||||
if (encoded.length < 2)
|
if (encoded.length < 2)
|
||||||
throw new IllegalArgumentException("encoding too short $encoded.length");
|
throw new IllegalArgumentException("encoding too short $encoded.length");
|
||||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
||||||
@@ -91,9 +91,12 @@ public class DataUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String readTillRN(InputStream is) throws IOException {
|
public static String readTillRN(InputStream is) throws IOException {
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||||
int read = is.read();
|
int read = is.read();
|
||||||
|
if (System.currentTimeMillis() - start > Constants.MAX_HEADER_TIME)
|
||||||
|
throw new IOException("header taking too long");
|
||||||
if (read == -1)
|
if (read == -1)
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
if (read == '\r') {
|
if (read == '\r') {
|
||||||
|
8
core/src/main/resources/defaults/i2p.properties
Normal file
8
core/src/main/resources/defaults/i2p.properties
Normal 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
|
@@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
|
|||||||
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||||
|
|
||||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||||
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
|
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null, null)
|
||||||
acceptor.start()
|
acceptor.start()
|
||||||
Thread.sleep(100)
|
Thread.sleep(100)
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,8 @@ class FileTreeTest {
|
|||||||
File b = new File(a, "b")
|
File b = new File(a, "b")
|
||||||
File c = new File(b, "c")
|
File c = new File(b, "c")
|
||||||
|
|
||||||
FileTree tree = new FileTree()
|
FileTree<Void> tree = new FileTree<>()
|
||||||
tree.add(c)
|
tree.add(c,null)
|
||||||
|
|
||||||
assert tree.root.children.size() == 1
|
assert tree.root.children.size() == 1
|
||||||
assert tree.fileToNode.size() == 3
|
assert tree.fileToNode.size() == 3
|
||||||
@@ -28,15 +28,110 @@ class FileTreeTest {
|
|||||||
File c = new File(b, "c")
|
File c = new File(b, "c")
|
||||||
File d = new File(b, "d")
|
File d = new File(b, "d")
|
||||||
|
|
||||||
FileTree tree = new FileTree()
|
FileTree<Void> tree = new FileTree<>()
|
||||||
tree.add(c)
|
tree.add(c,null)
|
||||||
|
|
||||||
assert tree.fileToNode.size() == 3
|
assert tree.fileToNode.size() == 3
|
||||||
|
|
||||||
tree.add(d)
|
tree.add(d, null)
|
||||||
assert tree.fileToNode.size() == 4
|
assert tree.fileToNode.size() == 4
|
||||||
|
|
||||||
tree.remove(d)
|
tree.remove(d)
|
||||||
assert tree.fileToNode.size() == 3
|
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
59
doc/architecture.md
Normal 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
98
doc/chat.md
Normal 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.
|
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable file
26
docker/rootfs/etc/cont-init.d/00-app-user-map.sh
Executable 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
|
34
docker/rootfs/muwire/.MuWire/MuWire.properties
Normal file
34
docker/rootfs/muwire/.MuWire/MuWire.properties
Normal 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
|
1
docker/rootfs/muwire/.MuWire/i2p.properties
Normal file
1
docker/rootfs/muwire/.MuWire/i2p.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
i2cp.tcp.host=172.17.0.1
|
7
docker/rootfs/startapp.sh
Normal file
7
docker/rootfs/startapp.sh
Normal 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
|
@@ -1,6 +1,6 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.6.4
|
version = 0.6.9
|
||||||
i2pVersion = 0.9.43
|
i2pVersion = 0.9.44
|
||||||
groovyVersion = 2.4.15
|
groovyVersion = 2.4.15
|
||||||
slf4jVersion = 1.7.25
|
slf4jVersion = 1.7.25
|
||||||
spockVersion = 1.1-groovy-2.4
|
spockVersion = 1.1-groovy-2.4
|
||||||
@@ -16,6 +16,6 @@ author = zab@mail.i2p
|
|||||||
signer = zab@mail.i2p
|
signer = zab@mail.i2p
|
||||||
keystorePassword=changeit
|
keystorePassword=changeit
|
||||||
websiteURL=http://muwire.i2p
|
websiteURL=http://muwire.i2p
|
||||||
updateURLsu3=http://muwire.i2p/MuWire.su3
|
updateURLsu3=http://muwire.i2p/MuWire-update.su3
|
||||||
|
|
||||||
pack200=true
|
pack200=true
|
||||||
|
@@ -121,4 +121,9 @@ mvcGroups {
|
|||||||
view = 'com.muwire.gui.ChatRoomView'
|
view = 'com.muwire.gui.ChatRoomView'
|
||||||
controller = 'com.muwire.gui.ChatRoomController'
|
controller = 'com.muwire.gui.ChatRoomController'
|
||||||
}
|
}
|
||||||
|
'chat-monitor' {
|
||||||
|
model = 'com.muwire.gui.ChatMonitorModel'
|
||||||
|
view = 'com.muwire.gui.ChatMonitorView'
|
||||||
|
controller = 'com.muwire.gui.ChatMonitorController'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -47,6 +47,7 @@ class BrowseController {
|
|||||||
|
|
||||||
void onUIResultEvent(UIResultEvent e) {
|
void onUIResultEvent(UIResultEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
|
model.chatActionEnabled = e.chat
|
||||||
model.results << e
|
model.results << e
|
||||||
model.resultCount = model.results.size()
|
model.resultCount = model.results.size()
|
||||||
view.resultsTable.model.fireTableDataChanged()
|
view.resultsTable.model.fireTableDataChanged()
|
||||||
@@ -64,8 +65,11 @@ class BrowseController {
|
|||||||
def selectedResults = view.selectedResults()
|
def selectedResults = view.selectedResults()
|
||||||
if (selectedResults == null || selectedResults.isEmpty())
|
if (selectedResults == null || selectedResults.isEmpty())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def group = application.mvcGroupManager.getGroups()['MainFrame']
|
||||||
|
|
||||||
selectedResults.removeAll {
|
selectedResults.removeAll {
|
||||||
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
|
!group.model.canDownload(it.infohash)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedResults.each { result ->
|
selectedResults.each { result ->
|
||||||
@@ -74,11 +78,11 @@ class BrowseController {
|
|||||||
result : [result],
|
result : [result],
|
||||||
sources : [model.host.destination],
|
sources : [model.host.destination],
|
||||||
target : file,
|
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()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,4 +117,13 @@ class BrowseController {
|
|||||||
params['core'] = core
|
params['core'] = core
|
||||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ControllerAction
|
||||||
|
void chat() {
|
||||||
|
dismiss()
|
||||||
|
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
|
||||||
|
|
||||||
|
mainFrameGroup.controller.startChat(model.host)
|
||||||
|
mainFrameGroup.view.showChatWindow.call()
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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
|
||||||
|
}
|
@@ -63,7 +63,8 @@ class ChatRoomController {
|
|||||||
|
|
||||||
if (command.action == ChatAction.JOIN) {
|
if (command.action == ChatAction.JOIN) {
|
||||||
String newRoom = command.payload
|
String newRoom = command.payload
|
||||||
if (!mvcGroup.parentGroup.childrenGroups.containsKey(newRoom)) {
|
String groupId = model.host.getHumanReadableName()+"-"+newRoom
|
||||||
|
if (!mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
|
||||||
def params = [:]
|
def params = [:]
|
||||||
params['core'] = model.core
|
params['core'] = model.core
|
||||||
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
|
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
|
||||||
@@ -71,8 +72,9 @@ class ChatRoomController {
|
|||||||
params['console'] = false
|
params['console'] = false
|
||||||
params['host'] = model.host
|
params['host'] = model.host
|
||||||
params['roomTabName'] = newRoom
|
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) {
|
if (command.action == ChatAction.LEAVE && !model.console) {
|
||||||
@@ -110,6 +112,7 @@ class ChatRoomController {
|
|||||||
params['privateChat'] = true
|
params['privateChat'] = true
|
||||||
params['host'] = model.host
|
params['host'] = model.host
|
||||||
params['roomTabName'] = p.getHumanReadableName()
|
params['roomTabName'] = p.getHumanReadableName()
|
||||||
|
params['chatNotificator'] = view.chatNotificator
|
||||||
|
|
||||||
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
|
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
|
||||||
}
|
}
|
||||||
@@ -141,8 +144,19 @@ class ChatRoomController {
|
|||||||
view.refreshMembersTable()
|
view.refreshMembersTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void browse() {
|
||||||
|
Persona p = view.getSelectedPersona()
|
||||||
|
if (p == null)
|
||||||
|
return
|
||||||
|
String groupId = p.getHumanReadableName() + "-browse"
|
||||||
|
def params = [:]
|
||||||
|
params['host'] = p
|
||||||
|
params['core'] = model.core
|
||||||
|
mvcGroup.createMVCGroup("browse",groupId,params)
|
||||||
|
}
|
||||||
|
|
||||||
void leaveRoom() {
|
void leaveRoom() {
|
||||||
if (leftRoom)
|
if (leftRoom || model.privateChat)
|
||||||
return
|
return
|
||||||
leftRoom = true
|
leftRoom = true
|
||||||
long now = System.currentTimeMillis()
|
long now = System.currentTimeMillis()
|
||||||
@@ -180,6 +194,8 @@ class ChatRoomController {
|
|||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
view.roomTextArea.append(toDisplay)
|
view.roomTextArea.append(toDisplay)
|
||||||
trimLines()
|
trimLines()
|
||||||
|
if (!model.console)
|
||||||
|
view.chatNotificator.onMessage(mvcGroup.mvcId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,4 +249,34 @@ class ChatRoomController {
|
|||||||
view.roomTextArea.replaceRange(null, line0Start, line0End)
|
view.roomTextArea.replaceRange(null, line0Start, line0End)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void rejoinRoom() {
|
||||||
|
if (model.console || model.privateChat)
|
||||||
|
return
|
||||||
|
|
||||||
|
model.members.clear()
|
||||||
|
model.members.add(model.core.me)
|
||||||
|
|
||||||
|
UUID uuid = UUID.randomUUID()
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
String join = "/JOIN $model.room"
|
||||||
|
byte [] sig = ChatConnection.sign(uuid, now, ChatServer.CONSOLE, join, model.core.me, model.host, model.core.spk)
|
||||||
|
def event = new ChatMessageEvent(
|
||||||
|
uuid : uuid,
|
||||||
|
payload : join,
|
||||||
|
sender : model.core.me,
|
||||||
|
host : model.host,
|
||||||
|
room : ChatServer.CONSOLE,
|
||||||
|
chatTime : now,
|
||||||
|
sig : sig
|
||||||
|
)
|
||||||
|
model.core.eventBus.publish(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
void serverDisconnected() {
|
||||||
|
runInsideUIAsync {
|
||||||
|
model.members.clear()
|
||||||
|
view.membersTable?.model?.fireTableDataChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -15,6 +15,17 @@ class ChatServerController {
|
|||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
void disconnect() {
|
void disconnect() {
|
||||||
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
|
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" :
|
||||||
|
model.connect()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -23,6 +23,8 @@ class I2PStatusController {
|
|||||||
Router router = core.router
|
Router router = core.router
|
||||||
model.networkStatus = router._context.commSystem().status.toStatusString()
|
model.networkStatus = router._context.commSystem().status.toStatusString()
|
||||||
model.floodfill = router._context.netDb().floodfillEnabled()
|
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.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
|
||||||
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
|
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
|
||||||
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()
|
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()
|
||||||
|
@@ -3,17 +3,15 @@ package com.muwire.gui
|
|||||||
import griffon.core.GriffonApplication
|
import griffon.core.GriffonApplication
|
||||||
import griffon.core.artifact.GriffonController
|
import griffon.core.artifact.GriffonController
|
||||||
import griffon.core.controller.ControllerAction
|
import griffon.core.controller.ControllerAction
|
||||||
import griffon.core.mvc.MVCGroup
|
|
||||||
import griffon.core.mvc.MVCGroupConfiguration
|
|
||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
import groovy.json.StringEscapeUtils
|
|
||||||
import net.i2p.crypto.DSAEngine
|
import net.i2p.crypto.DSAEngine
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Signature
|
import net.i2p.data.Signature
|
||||||
import net.i2p.data.SigningPrivateKey
|
|
||||||
|
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
|
import java.awt.Toolkit
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
@@ -28,15 +26,11 @@ import com.muwire.core.Persona
|
|||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.SplitPattern
|
import com.muwire.core.SplitPattern
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
import com.muwire.core.download.DownloadStartedEvent
|
|
||||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
|
||||||
import com.muwire.core.download.UIDownloadPausedEvent
|
import com.muwire.core.download.UIDownloadPausedEvent
|
||||||
import com.muwire.core.download.UIDownloadResumedEvent
|
import com.muwire.core.download.UIDownloadResumedEvent
|
||||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
|
||||||
import com.muwire.core.files.FileUnsharedEvent
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.files.UIPersistFilesEvent
|
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.trust.RemoteTrustList
|
import com.muwire.core.trust.RemoteTrustList
|
||||||
@@ -87,8 +81,19 @@ class MainFrameController {
|
|||||||
search = search.trim()
|
search = search.trim()
|
||||||
if (search.length() == 0)
|
if (search.length() == 0)
|
||||||
return
|
return
|
||||||
if (search.length() > 128)
|
if (search.length() > 128) {
|
||||||
search = search.substring(0,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()
|
def uuid = UUID.randomUUID()
|
||||||
Map<String, Object> params = new HashMap<>()
|
Map<String, Object> params = new HashMap<>()
|
||||||
params["search-terms"] = search
|
params["search-terms"] = search
|
||||||
@@ -358,7 +363,6 @@ class MainFrameController {
|
|||||||
sf.each {
|
sf.each {
|
||||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
|
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
|
||||||
}
|
}
|
||||||
core.eventBus.publish(new UIPersistFilesEvent())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
@ControllerAction
|
||||||
@@ -458,6 +462,7 @@ class MainFrameController {
|
|||||||
def params = [:]
|
def params = [:]
|
||||||
params['core'] = model.core
|
params['core'] = model.core
|
||||||
params['host'] = model.core.me
|
params['host'] = model.core.me
|
||||||
|
params['chatNotificator'] = view.chatNotificator
|
||||||
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
|
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,13 +489,31 @@ class MainFrameController {
|
|||||||
startChat(p)
|
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) {
|
void startChat(Persona p) {
|
||||||
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
|
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
|
||||||
def params = [:]
|
def params = [:]
|
||||||
params['core'] = model.core
|
params['core'] = model.core
|
||||||
params['host'] = p
|
params['host'] = p
|
||||||
|
params['chatNotificator'] = view.chatNotificator
|
||||||
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
|
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
|
||||||
}
|
} else
|
||||||
|
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveMuWireSettings() {
|
void saveMuWireSettings() {
|
||||||
@@ -502,4 +525,4 @@ class MainFrameController {
|
|||||||
core = e.getNewValue()
|
core = e.getNewValue()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -155,6 +155,9 @@ class OptionsController {
|
|||||||
int maxChatLines = Integer.parseInt(view.maxChatLinesField.text)
|
int maxChatLines = Integer.parseInt(view.maxChatLinesField.text)
|
||||||
model.maxChatLines = maxChatLines
|
model.maxChatLines = maxChatLines
|
||||||
uiSettings.maxChatLines = maxChatLines
|
uiSettings.maxChatLines = maxChatLines
|
||||||
|
|
||||||
|
if (model.chatWelcomeFile != null)
|
||||||
|
settings.chatWelcomeFile = new File(model.chatWelcomeFile)
|
||||||
|
|
||||||
core.saveMuSettings()
|
core.saveMuSettings()
|
||||||
|
|
||||||
@@ -237,6 +240,19 @@ class OptionsController {
|
|||||||
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
|
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
|
@ControllerAction
|
||||||
void automaticFont() {
|
void automaticFont() {
|
||||||
model.automaticFontSize = true
|
model.automaticFontSize = true
|
||||||
|
@@ -44,6 +44,8 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
propsFile.withReader("UTF-8", {
|
propsFile.withReader("UTF-8", {
|
||||||
props.load(it)
|
props.load(it)
|
||||||
})
|
})
|
||||||
|
if (!props.containsKey("nickname"))
|
||||||
|
props.setProperty("nickname", selectNickname())
|
||||||
props = new MuWireSettings(props)
|
props = new MuWireSettings(props)
|
||||||
if (props.incompleteLocation == null)
|
if (props.incompleteLocation == null)
|
||||||
props.incompleteLocation = new File(home, "incompletes")
|
props.incompleteLocation = new File(home, "incompletes")
|
||||||
@@ -53,25 +55,7 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
props.incompleteLocation = new File(home, "incompletes")
|
props.incompleteLocation = new File(home, "incompletes")
|
||||||
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||||
props.updateType = System.getProperty("updateType","jar")
|
props.updateType = System.getProperty("updateType","jar")
|
||||||
def nickname
|
props.setNickname(selectNickname())
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def portableDownloads = System.getProperty("portable.downloads")
|
def portableDownloads = System.getProperty("portable.downloads")
|
||||||
@@ -116,5 +100,31 @@ class Ready extends AbstractLifecycleHandler {
|
|||||||
|
|
||||||
core.eventBus.publish(new UILoadedEvent())
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ class BrowseModel {
|
|||||||
@Observable boolean downloadActionEnabled
|
@Observable boolean downloadActionEnabled
|
||||||
@Observable boolean viewCommentActionEnabled
|
@Observable boolean viewCommentActionEnabled
|
||||||
@Observable boolean viewCertificatesActionEnabled
|
@Observable boolean viewCertificatesActionEnabled
|
||||||
|
@Observable boolean chatActionEnabled
|
||||||
@Observable int totalResults
|
@Observable int totalResults
|
||||||
@Observable int resultCount
|
@Observable int resultCount
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,8 @@ package com.muwire.gui
|
|||||||
|
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull
|
||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.chat.ChatCommand
|
import com.muwire.core.chat.ChatCommand
|
||||||
@@ -13,6 +15,7 @@ import com.muwire.core.chat.ChatMessageEvent
|
|||||||
import com.muwire.core.chat.UIConnectChatEvent
|
import com.muwire.core.chat.UIConnectChatEvent
|
||||||
|
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
|
import griffon.inject.MVCMember
|
||||||
import griffon.transform.Observable
|
import griffon.transform.Observable
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
@@ -20,11 +23,16 @@ import griffon.metadata.ArtifactProviderFor
|
|||||||
@Log
|
@Log
|
||||||
@ArtifactProviderFor(GriffonModel)
|
@ArtifactProviderFor(GriffonModel)
|
||||||
class ChatServerModel {
|
class ChatServerModel {
|
||||||
|
@MVCMember @Nonnull
|
||||||
|
ChatServerView view
|
||||||
|
|
||||||
Persona host
|
Persona host
|
||||||
Core core
|
Core core
|
||||||
|
|
||||||
@Observable boolean disconnectActionEnabled
|
@Observable boolean disconnectActionEnabled
|
||||||
|
@Observable String buttonText = "Disconnect"
|
||||||
@Observable ChatConnectionAttemptStatus status
|
@Observable ChatConnectionAttemptStatus status
|
||||||
|
@Observable boolean sayActionEnabled
|
||||||
|
|
||||||
volatile ChatLink link
|
volatile ChatLink link
|
||||||
volatile Thread poller
|
volatile Thread poller
|
||||||
@@ -32,40 +40,65 @@ class ChatServerModel {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String, String> params) {
|
void mvcGroupInit(Map<String, String> params) {
|
||||||
disconnectActionEnabled = host != core.me // can't disconnect from myself
|
disconnectActionEnabled = host != core.me // can't disconnect from myself
|
||||||
|
core.eventBus.register(ChatConnectionEvent.class, this)
|
||||||
core.eventBus.with {
|
|
||||||
register(ChatConnectionEvent.class, this)
|
connect()
|
||||||
publish(new UIConnectChatEvent(host : host))
|
}
|
||||||
|
|
||||||
|
void connect() {
|
||||||
|
runInsideUIAsync {
|
||||||
|
buttonText = "Disconnect"
|
||||||
}
|
}
|
||||||
|
core.eventBus.publish(new UIConnectChatEvent(host : host))
|
||||||
|
}
|
||||||
|
|
||||||
|
void mvcGroupDestroy() {
|
||||||
|
stopPoller()
|
||||||
|
core.eventBus.unregister(ChatConnectionEvent.class, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPoller() {
|
||||||
|
if (running)
|
||||||
|
return
|
||||||
running = true
|
running = true
|
||||||
poller = new Thread({eventLoop()} as Runnable)
|
poller = new Thread({eventLoop()} as Runnable)
|
||||||
poller.setDaemon(true)
|
poller.setDaemon(true)
|
||||||
poller.start()
|
poller.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
void mvcGroupDestroy() {
|
private void stopPoller() {
|
||||||
running = false
|
running = false
|
||||||
poller?.interrupt()
|
poller?.interrupt()
|
||||||
|
link = null
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChatConnectionEvent(ChatConnectionEvent e) {
|
void onChatConnectionEvent(ChatConnectionEvent e) {
|
||||||
if (e.persona == host) {
|
if (e.persona != host)
|
||||||
runInsideUIAsync {
|
|
||||||
status = e.status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatLink link = e.connection
|
|
||||||
if (link == null)
|
|
||||||
return
|
return
|
||||||
if (link.getPersona() == host)
|
|
||||||
this.link = link
|
runInsideUIAsync {
|
||||||
else if (link.getPersona() == null && host == core.me)
|
status = e.status
|
||||||
this.link = link
|
sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
|
||||||
|
ChatLink link = e.connection
|
||||||
|
if (link == null)
|
||||||
|
return
|
||||||
|
this.link = e.connection
|
||||||
|
|
||||||
|
startPoller()
|
||||||
|
|
||||||
|
mvcGroup.childrenGroups.each {k,v ->
|
||||||
|
v.controller.rejoinRoom()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopPoller()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void eventLoop() {
|
private void eventLoop() {
|
||||||
|
Thread.sleep(1000)
|
||||||
while(running) {
|
while(running) {
|
||||||
ChatLink link = this.link
|
ChatLink link = this.link
|
||||||
if (link == null || !link.isUp()) {
|
if (link == null || !link.isUp()) {
|
||||||
@@ -106,6 +139,7 @@ class ChatServerModel {
|
|||||||
params['privateChat'] = true
|
params['privateChat'] = true
|
||||||
params['host'] = host
|
params['host'] = host
|
||||||
params['roomTabName'] = e.sender.getHumanReadableName()
|
params['roomTabName'] = e.sender.getHumanReadableName()
|
||||||
|
params['chatNotificator'] = view.chatNotificator
|
||||||
|
|
||||||
mvcGroup.createMVCGroup("chat-room",groupId, params)
|
mvcGroup.createMVCGroup("chat-room",groupId, params)
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,8 @@ class I2PStatusModel {
|
|||||||
@Observable int ssuConnections
|
@Observable int ssuConnections
|
||||||
@Observable String networkStatus
|
@Observable String networkStatus
|
||||||
@Observable boolean floodfill
|
@Observable boolean floodfill
|
||||||
|
@Observable String myCountry
|
||||||
|
@Observable boolean strictCountry
|
||||||
@Observable int participatingTunnels
|
@Observable int participatingTunnels
|
||||||
@Observable int activePeers
|
@Observable int activePeers
|
||||||
@Observable int receiveBps
|
@Observable int receiveBps
|
||||||
|
@@ -106,6 +106,8 @@ class MainFrameModel {
|
|||||||
@Observable boolean subscribeButtonEnabled
|
@Observable boolean subscribeButtonEnabled
|
||||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||||
@Observable boolean markDistrustedButtonEnabled
|
@Observable boolean markDistrustedButtonEnabled
|
||||||
|
@Observable boolean browseFromTrustedButtonEnabled
|
||||||
|
@Observable boolean chatFromTrustedButtonEnabled
|
||||||
@Observable boolean markNeutralFromDistrustedButtonEnabled
|
@Observable boolean markNeutralFromDistrustedButtonEnabled
|
||||||
@Observable boolean markTrustedButtonEnabled
|
@Observable boolean markTrustedButtonEnabled
|
||||||
@Observable boolean reviewButtonEnabled
|
@Observable boolean reviewButtonEnabled
|
||||||
@@ -361,6 +363,8 @@ class MainFrameModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||||
|
if (e.source == "PersisterService")
|
||||||
|
return
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
shared << e.loadedFile
|
shared << e.loadedFile
|
||||||
loadedFiles = shared.size()
|
loadedFiles = shared.size()
|
||||||
|
@@ -61,6 +61,7 @@ class OptionsModel {
|
|||||||
@Observable int maxChatConnections
|
@Observable int maxChatConnections
|
||||||
@Observable boolean advertiseChat
|
@Observable boolean advertiseChat
|
||||||
@Observable int maxChatLines
|
@Observable int maxChatLines
|
||||||
|
@Observable String chatWelcomeFile
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
MuWireSettings settings = application.context.get("muwire-settings")
|
MuWireSettings settings = application.context.get("muwire-settings")
|
||||||
@@ -114,5 +115,6 @@ class OptionsModel {
|
|||||||
maxChatConnections = settings.maxChatConnections
|
maxChatConnections = settings.maxChatConnections
|
||||||
advertiseChat = settings.advertiseChat
|
advertiseChat = settings.advertiseChat
|
||||||
maxChatLines = uiSettings.maxChatLines
|
maxChatLines = uiSettings.maxChatLines
|
||||||
|
chatWelcomeFile = settings.chatWelcomeFile?.getAbsolutePath()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.gui
|
package com.muwire.gui
|
||||||
|
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
import griffon.core.artifact.GriffonModel
|
import griffon.core.artifact.GriffonModel
|
||||||
@@ -21,6 +22,6 @@ class SharedFileModel {
|
|||||||
public void mvcGroupInit(Map<String,String> args) {
|
public void mvcGroupInit(Map<String,String> args) {
|
||||||
searchers.addAll(sf.getSearches())
|
searchers.addAll(sf.getSearches())
|
||||||
downloaders.addAll(sf.getDownloaders())
|
downloaders.addAll(sf.getDownloaders())
|
||||||
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
|
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(new InfoHash(sf.getRoot()),[]))
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -39,6 +39,8 @@ class BrowseView {
|
|||||||
def p
|
def p
|
||||||
def resultsTable
|
def resultsTable
|
||||||
def lastSortEvent
|
def lastSortEvent
|
||||||
|
def sequentialDownloadCheckbox
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
int rowHeight = application.context.get("row-height")
|
int rowHeight = application.context.get("row-height")
|
||||||
mainFrame = application.windowManager.findWindow("main-frame")
|
mainFrame = application.windowManager.findWindow("main-frame")
|
||||||
@@ -66,7 +68,10 @@ class BrowseView {
|
|||||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||||
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
|
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
|
||||||
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||||
|
button(text : "Chat", enabled : bind {model.chatActionEnabled}, chatAction)
|
||||||
button(text : "Dismiss", dismissAction)
|
button(text : "Dismiss", dismissAction)
|
||||||
|
label(text : "Download sequentially")
|
||||||
|
sequentialDownloadCheckbox = checkBox()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +111,9 @@ class BrowseView {
|
|||||||
else
|
else
|
||||||
model.viewCommentActionEnabled = false
|
model.viewCommentActionEnabled = false
|
||||||
|
|
||||||
|
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
|
||||||
rows.each {
|
rows.each {
|
||||||
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
|
downloadActionEnabled &= mainFrameGroup.model.canDownload(model.results[it].infohash)
|
||||||
}
|
}
|
||||||
model.downloadActionEnabled = downloadActionEnabled
|
model.downloadActionEnabled = downloadActionEnabled
|
||||||
|
|
||||||
|
62
gui/griffon-app/views/com/muwire/gui/ChatMonitorView.groovy
Normal file
62
gui/griffon-app/views/com/muwire/gui/ChatMonitorView.groovy
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
|
|||||||
import javax.swing.SpringLayout.Constraints
|
import javax.swing.SpringLayout.Constraints
|
||||||
|
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.chat.ChatConnectionAttemptStatus
|
||||||
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
@@ -28,29 +29,33 @@ class ChatRoomView {
|
|||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
ChatRoomController controller
|
ChatRoomController controller
|
||||||
|
|
||||||
|
ChatNotificator chatNotificator
|
||||||
|
|
||||||
def pane
|
def pane
|
||||||
def parent
|
def parent
|
||||||
def sayField
|
def sayField
|
||||||
def roomTextArea
|
def roomTextArea
|
||||||
|
def textScrollPane
|
||||||
def membersTable
|
def membersTable
|
||||||
def lastMembersTableSortEvent
|
def lastMembersTableSortEvent
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
int rowHeight = application.context.get("row-height")
|
int rowHeight = application.context.get("row-height")
|
||||||
|
def parentModel = mvcGroup.parentGroup.model
|
||||||
if (model.console || model.privateChat) {
|
if (model.console || model.privateChat) {
|
||||||
pane = builder.panel {
|
pane = builder.panel {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
panel(constraints : BorderLayout.CENTER) {
|
panel(constraints : BorderLayout.CENTER) {
|
||||||
gridLayout(rows : 1, cols : 1)
|
gridLayout(rows : 1, cols : 1)
|
||||||
scrollPane {
|
textScrollPane = scrollPane {
|
||||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel(constraints : BorderLayout.SOUTH) {
|
panel(constraints : BorderLayout.SOUTH) {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
||||||
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
||||||
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
|
button(enabled : bind {parentModel.sayActionEnabled},text : "Say", constraints : BorderLayout.EAST, sayAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +77,7 @@ class ChatRoomView {
|
|||||||
}
|
}
|
||||||
panel {
|
panel {
|
||||||
gridLayout(rows : 1, cols : 1)
|
gridLayout(rows : 1, cols : 1)
|
||||||
scrollPane {
|
textScrollPane = scrollPane {
|
||||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,12 +86,15 @@ class ChatRoomView {
|
|||||||
panel(constraints : BorderLayout.SOUTH) {
|
panel(constraints : BorderLayout.SOUTH) {
|
||||||
borderLayout()
|
borderLayout()
|
||||||
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
||||||
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
||||||
button(text : "Say", constraints : BorderLayout.EAST, sayAction)
|
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) {
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
@@ -134,6 +142,9 @@ class ChatRoomView {
|
|||||||
JMenuItem privateChat = new JMenuItem("Start Private Chat")
|
JMenuItem privateChat = new JMenuItem("Start Private Chat")
|
||||||
privateChat.addActionListener({controller.privateMessage()})
|
privateChat.addActionListener({controller.privateMessage()})
|
||||||
menu.add(privateChat)
|
menu.add(privateChat)
|
||||||
|
JMenuItem browse = new JMenuItem("Browse")
|
||||||
|
browse.addActionListener({controller.browse()})
|
||||||
|
menu.add(browse)
|
||||||
JMenuItem markTrusted = new JMenuItem("Mark Trusted")
|
JMenuItem markTrusted = new JMenuItem("Mark Trusted")
|
||||||
markTrusted.addActionListener({controller.markTrusted()})
|
markTrusted.addActionListener({controller.markTrusted()})
|
||||||
menu.add(markTrusted)
|
menu.add(markTrusted)
|
||||||
@@ -165,6 +176,7 @@ class ChatRoomView {
|
|||||||
int index = parent.indexOfComponent(pane)
|
int index = parent.indexOfComponent(pane)
|
||||||
parent.removeTabAt(index)
|
parent.removeTabAt(index)
|
||||||
controller.leaveRoom()
|
controller.leaveRoom()
|
||||||
|
chatNotificator.roomClosed(mvcGroup.mvcId)
|
||||||
mvcGroup.destroy()
|
mvcGroup.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -19,19 +19,22 @@ class ChatServerView {
|
|||||||
ChatServerModel model
|
ChatServerModel model
|
||||||
@MVCMember @Nonnull
|
@MVCMember @Nonnull
|
||||||
ChatServerController controller
|
ChatServerController controller
|
||||||
|
|
||||||
|
ChatNotificator chatNotificator
|
||||||
|
|
||||||
def pane
|
def pane
|
||||||
def parent
|
def parent
|
||||||
|
def childPane
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
pane = builder.panel {
|
pane = builder.panel {
|
||||||
borderLayout()
|
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) {
|
panel(constraints : BorderLayout.SOUTH) {
|
||||||
gridLayout(rows : 1, cols : 3)
|
gridLayout(rows : 1, cols : 3)
|
||||||
panel {}
|
panel {}
|
||||||
panel {
|
panel {
|
||||||
button(text : "Disconnect", enabled : bind {model.disconnectActionEnabled}, disconnectAction)
|
button(text : bind {model.buttonText}, enabled : bind {model.disconnectActionEnabled}, disconnectAction)
|
||||||
}
|
}
|
||||||
panel {
|
panel {
|
||||||
label(text : "Connection Status ")
|
label(text : "Connection Status ")
|
||||||
@@ -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) {
|
void mvcGroupInit(Map<String,String> args) {
|
||||||
@@ -69,11 +75,16 @@ class ChatServerView {
|
|||||||
params['roomTabName'] = 'Console'
|
params['roomTabName'] = 'Console'
|
||||||
params['console'] = true
|
params['console'] = true
|
||||||
params['host'] = model.host
|
params['host'] = model.host
|
||||||
|
params['chatNotificator'] = chatNotificator
|
||||||
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
|
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
def closeTab = {
|
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)
|
int index = parent.indexOfComponent(pane)
|
||||||
parent.removeTabAt(index)
|
parent.removeTabAt(index)
|
||||||
mvcGroup.destroy()
|
mvcGroup.destroy()
|
||||||
|
@@ -45,6 +45,10 @@ class I2PStatusView {
|
|||||||
label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
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 : "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 : 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),
|
panel(border : titledBorder(title : "Connections", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||||
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
||||||
|
@@ -35,6 +35,7 @@ import javax.swing.tree.TreePath
|
|||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.Core
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.MuWireSettings
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.download.Downloader
|
import com.muwire.core.download.Downloader
|
||||||
@@ -78,8 +79,10 @@ class MainFrameView {
|
|||||||
|
|
||||||
|
|
||||||
UISettings settings
|
UISettings settings
|
||||||
|
ChatNotificator chatNotificator
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
|
chatNotificator = new ChatNotificator(application.getMvcGroupManager())
|
||||||
settings = application.context.get("ui-settings")
|
settings = application.context.get("ui-settings")
|
||||||
int rowHeight = application.context.get("row-height")
|
int rowHeight = application.context.get("row-height")
|
||||||
builder.with {
|
builder.with {
|
||||||
@@ -129,6 +132,13 @@ class MainFrameView {
|
|||||||
env['core'] = model.core
|
env['core'] = model.core
|
||||||
mvcGroup.createMVCGroup("certificate-control",env)
|
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()
|
borderLayout()
|
||||||
@@ -280,7 +290,7 @@ class MainFrameView {
|
|||||||
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
|
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
|
||||||
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
|
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
|
||||||
Core core = application.context.get("core")
|
Core core = application.context.get("core")
|
||||||
core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
|
||||||
})
|
})
|
||||||
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
|
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
|
||||||
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
|
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
|
||||||
@@ -434,8 +444,8 @@ class MainFrameView {
|
|||||||
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
|
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 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 : "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 : "Browse", enabled : bind{model.browseFromTrustedButtonEnabled}, constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
|
||||||
button(text : "Chat", constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
|
button(text : "Chat", enabled : bind{model.chatFromTrustedButtonEnabled} ,constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panel (border : etchedBorder()){
|
panel (border : etchedBorder()){
|
||||||
@@ -490,7 +500,11 @@ class MainFrameView {
|
|||||||
}
|
}
|
||||||
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
|
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
|
||||||
borderLayout()
|
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) {
|
panel (constraints : BorderLayout.EAST) {
|
||||||
label("Connections:")
|
label("Connections:")
|
||||||
label(text : bind {model.connections})
|
label(text : bind {model.connections})
|
||||||
@@ -511,7 +525,9 @@ class MainFrameView {
|
|||||||
public boolean importData(TransferHandler.TransferSupport support) {
|
public boolean importData(TransferHandler.TransferSupport support) {
|
||||||
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
|
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
|
||||||
files.each {
|
files.each {
|
||||||
model.core.eventBus.publish(new FileSharedEvent(file : it))
|
File canonical = it.getCanonicalFile()
|
||||||
|
model.core.fileManager.negativeTree.remove(canonical)
|
||||||
|
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
||||||
}
|
}
|
||||||
showUploadsWindow.call()
|
showUploadsWindow.call()
|
||||||
true
|
true
|
||||||
@@ -520,6 +536,7 @@ class MainFrameView {
|
|||||||
|
|
||||||
mainFrame.addWindowListener(new WindowAdapter(){
|
mainFrame.addWindowListener(new WindowAdapter(){
|
||||||
public void windowClosing(WindowEvent e) {
|
public void windowClosing(WindowEvent e) {
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
if (application.getContext().get("tray-icon")) {
|
if (application.getContext().get("tray-icon")) {
|
||||||
if (settings.closeWarning) {
|
if (settings.closeWarning) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
@@ -533,6 +550,13 @@ class MainFrameView {
|
|||||||
} else {
|
} else {
|
||||||
closeApplication()
|
closeApplication()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
public void windowDeactivated(WindowEvent e) {
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
|
}
|
||||||
|
public void windowActivated(WindowEvent e) {
|
||||||
|
if (!model.chatPaneButtonEnabled)
|
||||||
|
chatNotificator.mainWindowActivated()
|
||||||
}})
|
}})
|
||||||
|
|
||||||
// search field
|
// search field
|
||||||
@@ -773,10 +797,14 @@ class MainFrameView {
|
|||||||
model.subscribeButtonEnabled = false
|
model.subscribeButtonEnabled = false
|
||||||
model.markDistrustedButtonEnabled = false
|
model.markDistrustedButtonEnabled = false
|
||||||
model.markNeutralFromTrustedButtonEnabled = false
|
model.markNeutralFromTrustedButtonEnabled = false
|
||||||
|
model.chatFromTrustedButtonEnabled = false
|
||||||
|
model.browseFromTrustedButtonEnabled = false
|
||||||
} else {
|
} else {
|
||||||
model.subscribeButtonEnabled = true
|
model.subscribeButtonEnabled = true
|
||||||
model.markDistrustedButtonEnabled = true
|
model.markDistrustedButtonEnabled = true
|
||||||
model.markNeutralFromTrustedButtonEnabled = true
|
model.markNeutralFromTrustedButtonEnabled = true
|
||||||
|
model.chatFromTrustedButtonEnabled = true
|
||||||
|
model.browseFromTrustedButtonEnabled = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -825,6 +853,10 @@ class MainFrameView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// chat tabs
|
||||||
|
def chatTabbedPane = builder.getVariable("chat-tabs")
|
||||||
|
chatTabbedPane.addChangeListener({e -> chatNotificator.serverTabChanged(e.getSource())})
|
||||||
|
|
||||||
// show tree by default
|
// show tree by default
|
||||||
showSharedFilesTree.call()
|
showSharedFilesTree.call()
|
||||||
|
|
||||||
@@ -880,7 +912,7 @@ class MainFrameView {
|
|||||||
String roots = ""
|
String roots = ""
|
||||||
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
|
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
|
||||||
SharedFile selected = iterator.next()
|
SharedFile selected = iterator.next()
|
||||||
String root = Base64.encode(selected.infoHash.getRoot())
|
String root = Base64.encode(selected.getRoot())
|
||||||
roots += root
|
roots += root
|
||||||
if (iterator.hasNext())
|
if (iterator.hasNext())
|
||||||
roots += "\n"
|
roots += "\n"
|
||||||
@@ -1027,6 +1059,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = true
|
model.monitorPaneButtonEnabled = true
|
||||||
model.trustPaneButtonEnabled = true
|
model.trustPaneButtonEnabled = true
|
||||||
model.chatPaneButtonEnabled = true
|
model.chatPaneButtonEnabled = true
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showDownloadsWindow = {
|
def showDownloadsWindow = {
|
||||||
@@ -1038,6 +1071,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = true
|
model.monitorPaneButtonEnabled = true
|
||||||
model.trustPaneButtonEnabled = true
|
model.trustPaneButtonEnabled = true
|
||||||
model.chatPaneButtonEnabled = true
|
model.chatPaneButtonEnabled = true
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showUploadsWindow = {
|
def showUploadsWindow = {
|
||||||
@@ -1049,6 +1083,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = true
|
model.monitorPaneButtonEnabled = true
|
||||||
model.trustPaneButtonEnabled = true
|
model.trustPaneButtonEnabled = true
|
||||||
model.chatPaneButtonEnabled = true
|
model.chatPaneButtonEnabled = true
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showMonitorWindow = {
|
def showMonitorWindow = {
|
||||||
@@ -1060,6 +1095,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = false
|
model.monitorPaneButtonEnabled = false
|
||||||
model.trustPaneButtonEnabled = true
|
model.trustPaneButtonEnabled = true
|
||||||
model.chatPaneButtonEnabled = true
|
model.chatPaneButtonEnabled = true
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showTrustWindow = {
|
def showTrustWindow = {
|
||||||
@@ -1071,6 +1107,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = true
|
model.monitorPaneButtonEnabled = true
|
||||||
model.trustPaneButtonEnabled = false
|
model.trustPaneButtonEnabled = false
|
||||||
model.chatPaneButtonEnabled = true
|
model.chatPaneButtonEnabled = true
|
||||||
|
chatNotificator.mainWindowDeactivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showChatWindow = {
|
def showChatWindow = {
|
||||||
@@ -1082,6 +1119,7 @@ class MainFrameView {
|
|||||||
model.monitorPaneButtonEnabled = true
|
model.monitorPaneButtonEnabled = true
|
||||||
model.trustPaneButtonEnabled = true
|
model.trustPaneButtonEnabled = true
|
||||||
model.chatPaneButtonEnabled = false
|
model.chatPaneButtonEnabled = false
|
||||||
|
chatNotificator.mainWindowActivated()
|
||||||
}
|
}
|
||||||
|
|
||||||
def showSharedFilesTable = {
|
def showSharedFilesTable = {
|
||||||
@@ -1106,6 +1144,7 @@ class MainFrameView {
|
|||||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||||
chooser.getSelectedFiles().each {
|
chooser.getSelectedFiles().each {
|
||||||
File canonical = it.getCanonicalFile()
|
File canonical = it.getCanonicalFile()
|
||||||
|
model.core.fileManager.negativeTree.remove(canonical)
|
||||||
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -280,13 +280,16 @@ class OptionsView {
|
|||||||
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
||||||
gridBagLayout()
|
gridBagLayout()
|
||||||
label(text : "Start chat server on startup", constraints : gbc(gridx: 0, gridy: 0, anchor: GridBagConstraints.LINE_START, weightx: 100))
|
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))
|
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))
|
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))
|
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))
|
panel(constraints : gbc(gridx: 0, gridy : 1, weighty: 100))
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
108
gui/src/main/groovy/com/muwire/gui/ChatNotificator.groovy
Normal file
108
gui/src/main/groovy/com/muwire/gui/ChatNotificator.groovy
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user