diff --git a/.gitmodules b/.gitmodules index 28770e46d05930c7f2b342a98e70062be3be85ef..97112d748142dd60fbe08f5eb2cc308e332b60a8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "duniter4j-cesium/src/main/cesium"] - path = duniter4j-cesium/src/main/cesium - url = git://github.com/duniter/cesium.git [submodule "duniter4j-elasticsearch/src/main/resources/cities"] path = duniter4j-elasticsearch/src/main/resources/cities url = git://github.com/David-Haim/CountriesToCitiesJSON.git diff --git a/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml b/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml index 46095a0f1a870e4e4400f7a142ea12d25fd12e77..184f1a706f38af1e13b7547098686fc32d0b36d2 100644 --- a/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml +++ b/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml @@ -249,4 +249,19 @@ duniter.subscription.enable: false # # Email subscription: URL to a Cesium site, for links in the email content (default: https://g1.duniter.fr) # -# duniter.subscription.email.cesium.url: 'http://domain.com/cesium' \ No newline at end of file +# duniter.subscription.email.cesium.url: 'http://domain.com/cesium' +# +# ---------------------------------- Duniter4j User (profile, message) module ------------------- +# +# +# Share link: `og:site_name` (default: 'Cesium') +# +# duniter.user.share.site.name: 'Cesium - Ğ1' +# +# Share link: `og:url` - URL to a Cesium site, for links in the email content (default: https://g1.duniter.fr) +# +# duniter.share.cesium.url: 'https://domain.com/cesium' +# +# Share link: Base URL of the ES cluster, to resolve `og:image` URL (default: none => /!\ Will use relative image path) +# +# duniter.share.base.url: 'https://data.domain.com' \ No newline at end of file diff --git a/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml b/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml index cd42a48aef7696c6608cfdeddb046cd5b3e8763a..124f71b8c5443388105ff26fe60d1b987a37dcab 100644 --- a/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml +++ b/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml @@ -251,4 +251,19 @@ duniter.subscription.enable: true # # Email subscription: URL to a Cesium site, for links in the email content (default: https://g1.duniter.fr) # -#duniter.subscription.email.cesium.url: 'https://domain.com/cesium' \ No newline at end of file +#duniter.subscription.email.cesium.url: 'https://domain.com/cesium' +# +# ---------------------------------- Duniter4j User (profile, message) module ------------------- +# +# +# Share link: og:site_name (default: 'Cesium') +# +# duniter.user.share.site.name: 'Cesium - Ğ1' +# +# Share link : URL to a Cesium site, for links in the email content (default: https://g1.duniter.fr) +# +#duniter.share.cesium.url: 'https://domain.com/cesium' +# +# Share link : Base URL of cluster, to resolve image (default: none => /!\ Will use relative image path) +# +#duniter.share.base.url: 'http://localhost:9200' \ No newline at end of file diff --git a/duniter4j-es-core/pom.xml b/duniter4j-es-core/pom.xml index a2d3ebe77f40fd014d06a026471b2eb6e76da2e6..4a5ecfd698a69dacfc8b15a0d2ed4a1e495abfc5 100644 --- a/duniter4j-es-core/pom.xml +++ b/duniter4j-es-core/pom.xml @@ -76,6 +76,14 @@ <artifactId>jackson-databind</artifactId> </dependency> + + <dependency> + <groupId>org.antlr</groupId> + <artifactId>stringtemplate</artifactId> + <version>${stringtemplate.version}</version> + <scope>compile</scope> + </dependency> + <!-- JNA (need for OS shutdown hook) --> <dependency> <groupId>net.java.dev.jna</groupId> diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/attachment/RestImageAttachmentAction.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/attachment/RestImageAttachmentAction.java index f861333dfd6230a9538538c5b4135ebbe8273bbc..e46a5926a10e170d5e51eadbb46875330ee65794 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/attachment/RestImageAttachmentAction.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/attachment/RestImageAttachmentAction.java @@ -95,4 +95,17 @@ public class RestImageAttachmentAction extends BaseRestHandler { } }); } + + + public static String computeImageUrl(String index, + String type, + String id, + String imageField, + String contentType) { + + int lastSlashIndex = contentType != null ? contentType.lastIndexOf('/') : -1; + String extension = (lastSlashIndex >= 0) ? contentType.substring(lastSlashIndex+1) : contentType; + + return String.format("/%s/%s/%s/_image/%s.%s", index, type, id, imageField, extension); + } } \ No newline at end of file diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/share/AbstractRestShareLinkAction.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/share/AbstractRestShareLinkAction.java new file mode 100644 index 0000000000000000000000000000000000000000..ff97fada10df8a93d416f847e86196295e3c881b --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/rest/share/AbstractRestShareLinkAction.java @@ -0,0 +1,91 @@ +package org.duniter.elasticsearch.rest.share; + +import org.apache.http.entity.ContentType; +import org.duniter.core.exception.BusinessException; +import org.duniter.core.util.Preconditions; +import org.duniter.core.util.StringUtils; +import org.duniter.elasticsearch.exception.DuniterElasticsearchException; +import org.duniter.elasticsearch.rest.XContentThrowableRestResponse; +import org.duniter.elasticsearch.util.opengraph.OGData; +import org.duniter.elasticsearch.util.springtemplate.STUtils; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.*; +import org.nuiton.i18n.I18n; +import org.stringtemplate.v4.*; + +import java.util.Locale; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestStatus.OK; + +public abstract class AbstractRestShareLinkAction extends BaseRestHandler { + + protected final ESLogger log; + + public interface OGDataResolver { + OGData resolve(String id) throws DuniterElasticsearchException, BusinessException; + } + + private OGDataResolver resolver; + private STGroup templates; + + public AbstractRestShareLinkAction(Settings settings, RestController controller, Client client, + String indexName, + String typeName, + OGDataResolver resolver + ) { + super(settings, controller, client); + log = Loggers.getLogger("duniter.rest." + indexName, settings, String.format("[%s]", indexName)); + controller.registerHandler(GET, + String.format("/%s/%s/{id}/_share", indexName, typeName), + this); + this.resolver = resolver; + + // Configure springtemplate engine + this.templates = STUtils.newSTGroup("org/duniter/elasticsearch/templates"); + Preconditions.checkNotNull(this.templates.getInstanceOf("html_share"), "Unable to load ST template for share page"); + + } + + @Override + protected void handleRequest(final RestRequest request, RestChannel restChannel, Client client) throws Exception { + String id = request.param("id"); + + try { + + OGData data = resolver.resolve(id); + Preconditions.checkNotNull(data); + Preconditions.checkNotNull(data.title); + + // Compute HTML content + ST template = templates.getInstanceOf("html_share"); + template.add("type", data.type); + template.add("title", data.title); + template.add("summary", StringUtils.truncate(data.description, 500)); + template.add("description", data.description); + template.add("siteName", data.siteName); + template.add("image", data.image); + template.add("url", data.url); + template.add("locale", data.locale); + if (StringUtils.isNotBlank(data.url)) { + Locale locale = data.locale != null ? new Locale(data.locale) : I18n.getDefaultLocale(); + template.add("redirectMessage", I18n.l(locale, "duniter4j.share.redirection.help")); + } + + String html = template.render(); + + restChannel.sendResponse(new BytesRestResponse(OK, ContentType.TEXT_HTML.getMimeType(), html)); + } + catch(DuniterElasticsearchException | BusinessException e) { + log.error(e.getMessage(), e); + restChannel.sendResponse(new XContentThrowableRestResponse(request, e)); + } + catch(Exception e) { + log.error(e.getMessage(), e); + } + } + +} diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/opengraph/OGData.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/opengraph/OGData.java new file mode 100644 index 0000000000000000000000000000000000000000..0530ead6288cd913cacb01fc7fc11883688e864b --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/opengraph/OGData.java @@ -0,0 +1,13 @@ +package org.duniter.elasticsearch.util.opengraph; + +public class OGData { + public String type; + public String title; + public String description; + public String image; + public String url; + public String locale; + public String imageType; + public String siteName; + +} diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/DateRenderer.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/DateRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..507fd6169aa0a88bfac49f54f0117eb58e2bf46a --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/DateRenderer.java @@ -0,0 +1,67 @@ +package org.duniter.elasticsearch.util.springtemplate; + +/*- + * #%L + * Duniter4j :: ElasticSearch Subscription plugin + * %% + * Copyright (C) 2014 - 2017 EIS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-3.0.html>. + * #L% + */ + +import org.stringtemplate.v4.AttributeRenderer; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public class DateRenderer implements AttributeRenderer { + + public DateRenderer() { + } + + public String toString(Object o, String formatString, Locale locale) { + if(formatString == null) { + formatString = "short"; + } + + Date d; + if(o instanceof Calendar) { + d = ((Calendar)o).getTime(); + } else { + d = (Date)o; + } + + Integer styleI = (Integer)org.stringtemplate.v4.DateRenderer.formatToInt.get(formatString); + Object f; + if(styleI == null) { + f = new SimpleDateFormat(formatString, locale); + } else { + int style = styleI.intValue(); + if(formatString.startsWith("date:")) { + f = DateFormat.getDateInstance(style, locale); + } else if(formatString.startsWith("time:")) { + f = DateFormat.getTimeInstance(style, locale); + } else { + f = DateFormat.getDateTimeInstance(style, style, locale); + } + } + + return ((DateFormat)f).format(d); + } +} \ No newline at end of file diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/STUtils.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/STUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cd0cb61b9f40bc0a38a92d411daa633c35253073 --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/STUtils.java @@ -0,0 +1,25 @@ +package org.duniter.elasticsearch.util.springtemplate; + +import org.stringtemplate.v4.DateRenderer; +import org.stringtemplate.v4.STGroup; +import org.stringtemplate.v4.STGroupDir; +import org.stringtemplate.v4.StringRenderer; + +import java.util.Date; + +public class STUtils { + + private STUtils() { + /*help class*/ + } + + public static STGroup newSTGroup(String dirName) { + // Configure springtemplate engine + STGroup templates = new STGroupDir(dirName, '$', '$'); + templates.registerRenderer(Date.class, new DateRenderer()); + templates.registerRenderer(String.class, new StringRenderer()); + return templates; + } + + +} diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/StringRenderer.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/StringRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..01e3ac3e5ebfcbaf053a22c9efed87f7250854c7 --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/util/springtemplate/StringRenderer.java @@ -0,0 +1,52 @@ +package org.duniter.elasticsearch.util.springtemplate; + +/*- + * #%L + * Duniter4j :: ElasticSearch Subscription plugin + * %% + * Copyright (C) 2014 - 2017 EIS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-3.0.html>. + * #L% + */ + +import org.duniter.core.client.model.ModelUtils; +import org.duniter.core.util.CollectionUtils; +import org.nuiton.i18n.I18n; + +import java.util.Locale; + +/** + * Add format capabilities: i18n, pubkey + * Created by blavenie on 10/04/17. + */ +public class StringRenderer extends org.stringtemplate.v4.StringRenderer{ + + @Override + public String toString(Object o, String formatString, Locale locale) { + return formatString == null ? (String)o : + (formatString.equals("pubkey") ? ModelUtils.minifyPubkey((String)o) : + (formatString.startsWith("i18n") ? toI18nString(o, formatString, locale) : + super.toString(o, formatString, locale))); + } + + protected String toI18nString(Object key, String formatString, Locale locale) { + String[] params = formatString.startsWith("i18n:") ? formatString.substring(5).split(",") : null; + if (CollectionUtils.isNotEmpty(params)) { + return I18n.l(locale, key.toString(), params); + } + return I18n.l(locale, key.toString()); + } +} diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties index 4ada9bb98edcc894fcc032a9291ca10215d21b6d..9a1bb01daa4734bbaf2aff4f6a3c15a5847620e3 100644 --- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties +++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties @@ -47,6 +47,7 @@ duniter4j.job.stopped= duniter4j.job.stopping= duniter4j.job.success= duniter4j.service.waitThenRetry=Error [%s]... will retry [%s/%s] +duniter4j.share.redirection.help=If you are not redirected automatically, follow this link\: duniter4j.task.issuer.system=System duniter4j.task.starting=Starting task... duniter4j.threadPool.clusterHealthStatus.changed=Cluster health status changed to [%s] diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties index a2854149be7a599f50ed4b957d98b93c52e422dd..43a57823caeaca108c0a12c4a9d9f8cb79baa43a 100644 --- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties +++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties @@ -49,6 +49,7 @@ duniter4j.job.stopped= duniter4j.job.stopping= duniter4j.job.success= duniter4j.service.waitThenRetry=Echec [%s]... tentative [%s/%s] +duniter4j.share.redirection.help=Si vous n'êtes pas redirigé automatiquement, cliquez sur le lien suivant \: duniter4j.task.issuer.system=Système duniter4j.task.starting=Démarrage du traitement... duniter4j.threadPool.clusterHealthStatus.changed=Cluster health status changed to [%s] diff --git a/duniter4j-es-subscription/src/main/resources/templates/cesium_logo.st b/duniter4j-es-core/src/main/resources/org/duniter/elasticsearch/templates/cesium_logo.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/cesium_logo.st rename to duniter4j-es-core/src/main/resources/org/duniter/elasticsearch/templates/cesium_logo.st diff --git a/duniter4j-es-core/src/main/resources/org/duniter/elasticsearch/templates/html_share.st b/duniter4j-es-core/src/main/resources/org/duniter/elasticsearch/templates/html_share.st new file mode 100644 index 0000000000000000000000000000000000000000..664245e3511d95a8b6d5d74c8825c691e38b178d --- /dev/null +++ b/duniter4j-es-core/src/main/resources/org/duniter/elasticsearch/templates/html_share.st @@ -0,0 +1,64 @@ +html_share(type, title, summary, description, image, siteName, locale, url, redirectMessage) ::= << +<html prefix="og: http://ogp.me/ns#"> + <head> + <meta charset="UTF-8"> + + $if(siteName)$ + <title>$siteName$ | $title$</title> + $else$ + <title>$title$</title> + $endif$ + + $if(type)$ + <meta property="og:type" content="$type$" /> + $else$ + <meta property="og:type" content="website" /> + $endif$ + + <meta property="og:title" content="$title$" /> + + $if(summary)$ + <meta property="og:description" content="$summary$" /> + $else$ + <meta property="og:description" content="$description$" /> + $endif$ + + $if(siteName)$ + <meta property="og:site_name" content="$siteName$" /> + $endif$ + + $if(image)$ + <meta property="og:image" content="$image$" /> + $endif$ + + $if(locale)$ + <meta property="og:locale" content="$locale$" /> + $endif$ + + $if(url)$ + <meta property="og:url" content="$url$" /> + <script type="text/javascript"> + window.location.href = "$url$" + </script> + <META HTTP-EQUIV="Refresh" CONTENT="0; URL=$url$"> + $endif$ + </head> + <body> + $if(image)$ + <p> + <img src="$image$"/> + </p> + $endif$ + + <h1>$title$</h1> + + <p>$description$</p> + + $if(url)$ + <p> + $redirectMessage$ <a href='$url$'>$title$</a>. + </p> + $endif$ + </body> +</html> +>> diff --git a/duniter4j-es-subscription/pom.xml b/duniter4j-es-subscription/pom.xml index c9beba4083d42dc7e961e6f0883ef92419a51fd9..c2ccb92692bfefbb8cd6eac45c731daf224d0934 100644 --- a/duniter4j-es-subscription/pom.xml +++ b/duniter4j-es-subscription/pom.xml @@ -46,7 +46,7 @@ <groupId>org.antlr</groupId> <artifactId>stringtemplate</artifactId> <version>${stringtemplate.version}</version> - <scope>compile</scope> + <scope>provided</scope> </dependency> <!-- Unit test --> diff --git a/duniter4j-es-subscription/src/main/java/org/duniter/elasticsearch/subscription/service/SubscriptionService.java b/duniter4j-es-subscription/src/main/java/org/duniter/elasticsearch/subscription/service/SubscriptionService.java index 871c8863dfa34d6654879fae2f3508fa16b4dc30..5f93f052166cc1abf1767bf27afacf380c409d42 100644 --- a/duniter4j-es-subscription/src/main/java/org/duniter/elasticsearch/subscription/service/SubscriptionService.java +++ b/duniter4j-es-subscription/src/main/java/org/duniter/elasticsearch/subscription/service/SubscriptionService.java @@ -51,6 +51,7 @@ import org.duniter.elasticsearch.user.service.AdminService; import org.duniter.elasticsearch.user.service.MailService; import org.duniter.elasticsearch.user.service.UserEventService; import org.duniter.elasticsearch.user.service.UserService; +import org.duniter.elasticsearch.util.springtemplate.STUtils; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.unit.TimeValue; import org.nuiton.i18n.I18n; @@ -76,6 +77,7 @@ public class SubscriptionService extends AbstractService { private UserEventService userEventService; private UserService userService; private String emailSubjectPrefix; + private STGroup templates; @Inject public SubscriptionService(Duniter4jClient client, @@ -100,6 +102,11 @@ public class SubscriptionService extends AbstractService { if (StringUtils.isNotBlank(emailSubjectPrefix)) { emailSubjectPrefix += " "; // add one trailing space } + + // Configure springtemplate engine + templates = STUtils.newSTGroup("org/duniter/elasticsearch/subscription/templates"); + Preconditions.checkNotNull(templates.getInstanceOf("text_email"), "Missing ST template {text_email}"); + Preconditions.checkNotNull(templates.getInstanceOf("html_email_content"), "Missing ST template {html_email_content}"); } public String create(String json) { @@ -308,11 +315,7 @@ public class SubscriptionService extends AbstractService { subscription.getContent().getLocale().split("-") : new String[]{"en", "GB"}; Locale issuerLocale = localParts.length >= 2 ? new Locale(localParts[0].toLowerCase(), localParts[1].toUpperCase()) : new Locale(localParts[0].toLowerCase()); - // Configure templates engine - STGroup templates = new STGroupDir("templates", '$', '$'); - templates.registerRenderer(Date.class, new DateRenderer()); - templates.registerRenderer(String.class, new StringRenderer()); - //templates.registerRenderer(Number.class, new NumberRenderer()); + // Compute text content final String text = fillTemplate( diff --git a/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/cesium_logo.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/cesium_logo.st new file mode 100644 index 0000000000000000000000000000000000000000..83cf445c1c6d63f4dfb4612f81a06253aff3769b --- /dev/null +++ b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/cesium_logo.st @@ -0,0 +1,6 @@ +cesium_logo(url, data) ::= << +$if(data)$<img height="144" width="144" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4AgRBwUClHNJ9QAAIABJREFUeNrsnXd8VvX1x9/n3mdkT5JAIOwle8gShDAE2TjAPWpbR7VWba3tT2v52f7Uah1tXThqtdYBrhBBmQkbka0s2SOMBLLHs+49vz+eAAkkECBAoJzX6/7xPPd+7/jezz37nC9cokt0iS7RJbpEl+gSXaJLdIlOieTSFJyYxr66ZDSGjK74n8+f+8uvHxzhvTQ74Lg0BSf7xORyUX5e8a8IM/QR4BKALgGoehr8pbazhAnewp33hhTtvzQhFyOAUjM0wi6mgQFJphJjKdEYRIpNlJqIWhiGEIFgqVAiio1SolAqkCcmeWpTEDDZu2A4B1Onk6QWNwG3WtAtyIHMSyi5GAGUOlEd/kIE2J/Ukp1T2ovv8L7h09Vd6MfltDHFQTgGDmxixCZahKgAxKpNEwN6IjRy+Ok04CsaqxIrx+iFipQghgvVAKgFUFAap6dyr93TNWzFCjxMFPuSEn2R0FVpmuyHu4GfIjQ6Zla8KNuBYlXyxGAtNqsdyppYH+unTBDrlEXibI3XMkLmjpasSwC6gGlguna2lV8I3A6EHLN7hcCbPuWjRWOlCGBwmibZJl0UumDRBSFB4FuUxRrFosyBkl9jrjlZI3DS0m+w9fD5LwHowgHOYFV+DwyuLKIoNOA9lFczxsqmk51n2GSN84dyuW1zJUIvhe0izDL9ZMy5Vg6d9EZUpX8aHQwHmjmSdYjoJQDVYRowVYcjPCHKFcfs2ojwit/m/dPlBndPUueGRLqaJv0VBoqyDZgW6iTj6xFyQjP/ym+0gSNAJ7OUpbMnSMElANU1jjNVB6jwF5Rex+xaLsozGav4sjaV2rsnqXNzA66whZFi0xGY7TD5YvYo2VbdmKEzNNzjYwB+ts+/VjZcAlBdsM6+0g4oz6KMrCw5WCUGaSh7BJIUohQiAYcBoTaUARiCBeSrkitKrm2QY8Aus4wdNeUUI7/S2GJlnMC1wFYx+ChjBMuqElcTVY3Mr7jCVlxGJPMzB0rgEoDOAw37RuN8fp61bX4qgoGWP51iIdSWMycf2KiwFvhBhFVEsDxzoHiq5ErL1bl5H1eh3GiDaQj/zFhBRlWcLzVd2wIdiGD2qSjmlwB0JtwmQx1SRF+ER9RmOIKziicsQNmNsB+lQIUCUQo5JhQhQpQqcQqxArFAfSAZTgw+BZ/ASlUWC8yKNJifPlpKjz1uUJr2tw3uVkUU3pk/moxjOdKQdG0csLkSJ/MyR8ieSwA6C9Rnsoa63IwSg2tFGaEQdcwhXuAjLP7m8LP9TBTU8ZPVleOmsUIzoCVCF4GuQAcgtJphHoSFqkw1hc8r+X0mqtG/G6mmcC9QrAZ/zxwpq495vjh3GENNg7VzRsj6SwCqDVKV1DQGq3CbCNcAkQq5AtEVOIQKfGAoj84ZKwdq6cIyfDquQj+uKGdwrvKLcYS5cQYMmovSF+UKFfoJJFTBnWyEpWIz2XLy4YIRkgNBL7p24yYRfiLKklLl799WuOehMzTc62eEqeycO1qWXQLQadKV0zXBsLhTlLuBliirxSTDthks0KnCi1pv2vx87jhZfArcxdwD0W4nUaZBlGUQJUK4KqECYZZNiMMqddl5u0OM2BSPOsLsE4gxUZtkEdohdFFocVxIRPGLMB14O8HD11MmiHXldE1wBLhPYaDCPxM9fHjYyz18urrLLIarUDBvOfPqahikTgJo0BfaQh38WpU7gVyFd0T5txhcocqrQET5oQHghVAHfzyR32WiqrFgKglAkl+oJ0q8GESLYpyQ/8x7vjXTfvtrGf7MSwz83cZTmNQoW+mC0BNoeew8CxxQ5QuBD8Rghy1cpjaPiJDt8PHU7OuCpv/4yeo6GMJwhOLqlO9LADreEvkjcB0wU2zerOdlWkE0IX4vbyncVOGrX2sKP5k7WlZWJXqunEw9QkkxlWQxSMSuWeDYYSLJYTiSQgkpWTCp/bp37v1Jr3tf+6Db6PuyjGOtORsKLXy+AL5iH4F8P948H9aeEjxFXg7Hy+rZSi8DequQeJy+pixGmCGQZwtDRGmO8K+cFby5bqL4uk9SZ2QDhqninbeKOXUNRHUCQIOmaRO1+KPCTQJTDJtn54wLKpCpadoSg89ROla46X9HCPdWsnYmqtG/Cw0NpYWapBjVK7kkunF0TSKuZSSxKRHUSwyjfpSTRqEOkkJN4g8ft3pZJg/fMZAX3p1Nt96DT+mZbMXjtTlY7GN/npf92WUcmJNF1IqDdCwM0Ea0wtwLAZSlKDOAhgTDLWvcXu6ZMUFyUzPUIYWMsE2K5o0iE+pO+OO8pnMMnaHhfh+P2xa/BP5jQ7v5Y2T74f0Dpupw4EOUmHKukyvCTzNGy5dHzvG5JnoN2hgmzdBgcLTiVxHqRFIbENutHs2aR9KiQThtQsxjou9ngQwhJNSkUWgojRJCoXUM9GsQ3LenhF3Pr2Hr94foqRCO4gD6IfQRZZGtfIlBZ08IH6RO0yczB8ry8ZP1mwNuRg2cSv+MMcz7rwfQwHS92eflORVWGMrlxwYyB6TpfQJ/r3CP36nNdfPGye7uy9UZk00rO8Blfog3ytFV7s/R/g2IHtiQy9pE0ykhhM5GVf6h80iNwmn8tytoXOjn4AtrWLZgH10VYgBThf5AD+AtAyLV4uXUdH15ymj5tM9k/doMYfTAr7R7xihZ8V8pwlK/0KZqMkmURIVH5o2VjGM0XiO1O8+h/LqiyNJI7gaQEtqrRQcR3Ee+AsG4ugnJg5Lp1DaGK0LM403q06EzEWGnQqUBSl9ey7ez99JNlegKu/YDS4H6KJ+mjuGlxTMJ9XkYZxssmz9aNp/s3N0nqXPFPeK/8DnQRDVSu/JLhCcE/koUL8w7Ju4zfrKaOSG8TdD6ArCAxz0e/u606WYYXAaYIiCFe9x92zYKv7YZndrHMNRpHNVdLjQKcxD2cNvigQ90jDj01AoyV+TQm2CuUn1gHLADgzsyppIsuTxOPWYYNiP7pmnJorGy90Tnjm1ICrDtrInqc6Ikp2vD1G7MVOFa06Rv5hj5S+bx4HHluPkYguBRKHXAdYaQ4XRzk2nQQcAMNTHuae1r0eyLcc/8/rLiZ7rEcdPZAo87JJTmbToRFh51VufH6ynltz+/mjDxx/+1N6kfDuZg00gWHRXMNEVpZwg3EscboSY+W5jvEAanTtaIE53bdJA/OE07XbAibEC6XiPwKspzmaP5W1UR6fGT1ZUTwmfAqPK/DonFL2wT92FrKikM5z2XcXmvJMaGmsTOm/Epm9ev5GcPP82FTv96ZSKNmrRiyOhbKv3/bTZr/7icEK9F6wpvzIOSSSQ3UUpTbJqmjmLqRKnevE9N0zsSvHxwOqm4540DpWaoI3WqPivKCyjXZo6Rl6sBj5nj5j9HwCPsQZiISYwBoYlhOJ7uRZcPBvF/qcncGWoSCzBg2PVs3bSWPTs3X9Dgydm/h+9XLGTwqJuP29crkU7ThtP8mqbMF6Gw3AEWAlytxSyLcrMPoXhuGr1PoqjMyAnlxgtGhF2VpskUkYHSyK90zhwjS6vTiw6G8C7C9Qqosg+LV0TxhTkwn+hOjw8G8nzvRO4zpZJyCcA9v/kL77z8+AUNoEl//S0/f/hpRKoWBqbgeLAj/T8eTGFCCEcsL1FaFZayQcFrOGjcf6o2q+4amSNlP0rToTM0vM4DaFC69vQLy0T4InOs3HqidNGBXXlW4bZy8/ugIbxiGuT/tA1NpwzliUHJ/MxhEFbd+KYt2xMZHcfyRTMvSPCsW7UYlzuEtp16nvTYxFAaTb6K7j9twyIJ5iUBxIpNhm3RBuHKE+lDpskUn4+H67QOlJqu16O8IsJdGaNl+gn9QFP1boVJ5YlfhwRebB1NyZOXMyE57Lj85WqpqCCXJ+4fx4vvzcU0L5wyN9u2+c1dQ3j8uQ+IT0w+NbFXRta989mb66PH4f8U5poGb8wdJVOqfT9T9WMs7s28pvYS1oxaBM/vgGcFBp8MPAPS9CqEVxFAKDEN/v5YF+Jeu5KnTwU8AJHRcfS76hq+mvzmBcV9vvniXS7vO/SUwQOQEErDT4dx+fAUlqtil3OCQbbN3wd9peOrHaj8CwcP1C0OpCoD0/mbwkDLz9AF18m+Ex0+JF0bB5SVQDyKv14or7/ajysTQ+l6urdgWQEeuWMQf3rlC6Ji6r47qLSkiMfuHs4L/5yNyx1yRuf6eheb/rKaxiLlsT/Fbxj8Yu5oebuqdzXgKxYGbK6urbq0M+JA4yermZrOP1Xo6XYx4GTgSc3QkAB8qRAPaOd6fP7RYG47E/AE5buD2+9/kvdee+qC4D7/fv1P3HDXb84YPADDG9PmhhZkIhwsZwlOW3lrYJo+N36yVs4eEFGUj51w13nnQOWOv/+4yrLbhOVuj5dyp5eK9vvyvj47qtR70vQdFe5CoVsCy17oQ8/afDGP3z+Wux95hiYt2tVZ8BQX5fPM7+7g/15Nqz19SrF/msmXO4rpLpCiYIjtx+Ep/jakcM//is9TcoQJuUKjSuNbvDjn+9C2tZEacloASs1QB0V8jBAdtW/5R0bAeufow9itpt5/xZYqnFm3qPBvAYkPYdWUq+gstWwFHti7E4fDeVp6xbkkVa3WbD9dKvSyb/wcNnotkoDGrpKDEeF5W6s81h8Ss6M0vs0DGWNl2rkXYRPV0ELeAeq7XIwzLOukgbp+n2tn4FUBcRrseG8gzeUsuBCSkpvUDDylWVD4Y3ArO3DOAVTb4AGIctPg4U6IQCGwSdTOrfalBzybVbj/vOhAA7rxD4QODg8jZw6TEjHcBiLFiBSD5KttVnKXp07XRg4Hb2AQjeD5Rz884Y7jnYLnlLy5UHoguPkKuVjo6kakNo1ir4Bf1NpZ7Uv3e7KATqnT9Yzzok7JcZI6VR9TSBVhwOFymbzkLt9KeeqFKXwze7TsOnz80M810RvgEdGgq31sU75rE82VXKKzRn/pSdebZrMbMRJOoLfYApPxczPw3DkB0IA0HY/wKwlwReY1cvCIPBcaiIItaIjJgQp6UkygkLGGcLMCUS5WPtiBvnX9BWjJLuwDlT3bZvJICGlwQQAoMZRm45ry/bQfaHjiB+UzFSadKYBqJMIGpWtPhDcMGJd5jVSysCSotCGQd7gyonu6htkFjLANBqmSJFD8xpXEG3Ju0kdOSjEdIKlfcItqedFxobvbMdRhqOdEx2SsYglC3MCp2v6sAmjYNxpnKx8DDxxb5DZ8urpFgpWhhk32YSU7AgYbEIkdjLCPacqqBmE0qTMzLFJ5u8jIbRJyVWNnWcAVURhwRRTajpAdiiwA5gJzMVjPRLFRpmqwUPMsibCJanh8fCQwad4Y+ejY3SU+Es1yCPo16Mga2I0rbSUGg95AhNvkx192oM8l7eTc0oN94jvPyY+fURSgHkq+ZfDXBaNlTqXvyOAL2+Zp4M9nhQOlduVJUUoyR1ctJx0Vco9DQznY/yu9TIVWomxAuAXQJ7tTakoQqLZd93tMiisKie5QaVOzchaE2gHU9qO2/3DfzTpFtm0j4Ly3fXkmgxBv2IxIzagcra9XSoZAq/5fakqtA2hAuvZBuMHh5ScnaMNWD8BWtMxCsegDLES4F8XZKILFVyTRBYLxn4dvT2XB7C/qNoKcMRjxvStt4qyc0rp/2fPsW/I0+5Y8Te7GyXXq9hdnpPPQ7QMoKsxjeGMGRLnIByyEbnYBlVJbp0wQH7DQMBhcqwAaOkPDBSbZys0n6m5h28SVK9JFhp9Uh8lGw6AM4XYg8OceR/WesPBI/vLW12zduIbH7h7Otk1r664lVrAWO2fBkU3zV9d5zrl7+yaeeGAcK5bM5unX04mMikXAvKP1EdM92jQZdWx8TIUMgUG1qgP5vLygwkvzx8qqasVbhjq0mEhRsE2SDIOs+FK+zQnlLyjONjEsbBJBv4pjQkLDufOBiezdtZW3X34cp8vNfY8+T0x8Yh0z5XeingoeancCEtOlTgKnqDCPj976C9t+/J57H32Opi0rG1Vjm9D/9fUsDgQlbd8DoTQHjuQBG8JcW3mo1jhQarr2Q4mfN1rePdFAq4xYQxGBKIF4bxlzC6IJQblLwffH7tX7IZIbt+DJFz/m6nF38MeHxvPR238h4PddMApqWGIXwpK6EZbUDXfM+XED2LbNzLT3+Z/7RtOxez+enTTtOPAAmAZhwxtRTDDaHW/A0Erm/HJWASGpadryjAE0frK6UJ50u7nnpMqmRYwIhgqtcbJkyQTJ9Xu5BYhtEcWyBmE0O9k5uvYexIv/mkNkVCwP3zmIpfOmXRAAim4+nJiWo4lpOZrwBj3O+fVXfZvBI3cO4lDOPl58dw59Uked8Pg72tATUBE82FzdN00jj1raYgssVqFHVVb4KQEo282vDeGvM66W3JMNNIUotWmsNj67mOVB5yY/A3i080m8oBXPYzoYNeFunn5tKssWfMMfHriGgryD59kUcyCG88iGceLKaPvgEuzt7wa3nR+d1Vv7buEMMr7+hIkvT+Gmnz2G0+U+6Zj4EJJbRbNfFb8atHQadK4ss1lhcDyAUttVn49+nA40OE2TLCF87mipaYZ6fYVkhNX1oWDwl9rOgh4xbpa3jeHyU52YyOg4HnziH+zcuoHwyPMbazUaDD81vGFjHzbnz7JZ36PfMHr0G3bK425oQfSfV4LYWBiMAxYewY/BCpTfHjvG5SAMKK4RBwrAXZFCjav0bJueImTZSuGUCeILmNwB8LO2eM5kgpq0uAyHo071QrgoaEAyvc2ghewT6FNRjJmwEuiCaiW3vE9OzoGMcu7TyTBYVlV30SotsOnaSGziRdltShChoowXIefqlOMae198pAEo3hncSrPADEOc0eXbycugfe+Nwff1Y3AOHasOIaxLHHkIPlup77aPRgfKG4EWD/mKlGPk00lzbh3l3KfNvNHVl4McM3tiBehlCFkqwfW3+qdpV6DZ5QksMOW/IF3DDkBxedaK6UYSeiKxNUzr3joLb+4uLNMm8GJrDGckrtEvYTRPPeu3PaEFDVccRA3Fb5lcDcysIIc32EpL4Eg6jm2dvC2O0X+qNhMHC2p6E/3TaSmCwxAOlcvPEgPGAtzVmkQuUfVUlgd7vyPgDqGkaQ/yet9GfvuBlE37JSUvtsfzxS/BV3rWLt+9Hl0dglcFP0qvSlaWstWWCjX4QR/RSTV0wxCKMkdKzdZ0VBXDprshrKtwYY8Kwx0Gm9vG0ua/AwkCzvDg5gir+bDlb6DucOzoo9+ZFRJNftfrKew6Cm/heopf7U7xO8Oxs1bW+l2bBq4WURxC8BtC/dTLqVh9sEVtWlV81xgnF2FG5mipsc2cOoMWtlJoBzgS3lAbp0D37vXY81/DSUw3xHcLbrEdajbmx68gsTllm+bga3i8w892hlHcoj/FHUcSCHdQ8uW9FE4aRGD1h7V661c1JBybw611hlT4JrYaxlEdKHUK4WJx0nz3U0vw8tPFcLLSobiOXFfoDJjXNaudrmAXtm7kR4s2o0WbwZt99P/iA3BoI0Qn4y/Kw3ZU/2FbIZGUNLocT9Mu2FERFKz7hIJX++Fb/Hqt3GJqI9ojWAq2BI529TBgm81R/51lEol5cou6xgBK/UKbAoHMkbLfdhxVrkTpJnCoewKXXdSG18FFaM6Co1vxluOPsb3YOfOwc+ahhZsOiwJY8SY0DZbA2VqzbnO+6IaUNuyMYQq+JpdRsG8++a/3x7vwlTN6jng3jcoj9AE1aD9Rg3qQbZItehRA4iJCa7C0ec05kEkHAqwGMO2jHEgNOiSFstGovRVx6iZzKfoRu2jTka1SsPVEtPJtaNIVRLC2LMCOb1hz0BoOSht1BQRHwEtJh0EUZS2g8NW+WOtPvzCxXSxlotgCYQs+IyiDQzlIsKXeYfM80jCCS2CdMYCGfaNxAmGZ17CzHDSOcu5jqNK+Z+LJkfpfoVobbozEVIzEVIhsA9szweWAkKBnvWzdTDzJHU75vL6YhngSWhGxaxn+xGYUdR5K/vK3KH6tP/a+Nad8vt6JhKkRbIRuuYPVweVLVvn6TNZQAFuIKyuhqFYA5PfSwVA2Hm5wbdtHxtVHCEtNPorcs0kBv49D2XvxesrOGSi8nlK+nT+dgFWDEIXhRCJaBjd/APZ/BwlHg9x+fxkYp1dXYLvCKG7ck5D9G3CUHsLTvAcFHQZSMPWXlE75ySk5JXsk0BS7vJO+XSkudtBhBntyi01co5OEMWoEoO6T1GlDs1JvhRwSIzhODZIFPB3jKph/tUyHsvfyzt+e4OarmjO0cwjXD2jI1V3DGNMrjj/9+ibWrVp8VgE05V8v8bt7RjJrzrfBoGr5JuI4saNx5VvQ7GjnOWtzJlb8GdbxiVCa0g1nUTau/D1gGJS2H0xReBgFr/Yl8P2nNTpNg3BamQal5dbXZRUssWLTRfT4yWra4C7PWDwhnbQuLDKZFghZSybIkc/eVgwJ6of1Qwx2OAzano2Xt3zxLP7065sozD9E2049GTTiRiKiYsg7lM3WjWvInDGF4dfddVYB1KJtZxIbNKZZz59gNq1h66Kl/4DmldsWlm6Yi7ddau34I5Muw527HVfeLnyxjQlEJVLc9WoCy98iZH064Te8dzIvllE/nMK9xdQXpcHQGRo+c5iUaLA1THhuHDFGKTUq2XXUAPSt1aA6r1b9hDDOyorD2378nv+5bzQOh5P/ezWNKwaNOe6Y/EPZZz2bsU/qqEr5Nra/BH/JUb+rMzwJw1khV/37j6FeMjjDjhG/nlq9L29cM9y5O46ACMDT6gqs/Cz075cTcfunENO02vHNI/HtDQoo0+enGfADECCAw19GnBjknjGAhkzWaL9N9LyRZFWyDsAqD9vWaxbF3rPx4p5/4qf4fV4mvjyFKwaOrvKY6sBTVJjH+tVLKCstoUGjZrRq1w2jGt1j9/ZNZO3aQvHBH4l0WzRv3hyX20108+EgBqXFhRTkHSQuoT7ukDB8RVnsX/M++QXFRESE0qjLbYTUK3fobp0NlFFIIiV795KQmIjD4SCwaQ6lUfXJyckjPj4GwxCy9uWwb99BnE4HLVs0Ijzs6NowB7Jz2bM3G5fTSaPkBGJjo6oBUdPjQOSPaUhBlyTs968j4rpJGA2rzqzpGI9r4T5QwRCbFsAPCAEEhwFJYtcCgLyhtDAsdhxblWEKliqgxLaJIa/Wuc+mtWz8/ju69hpYLXiqNHtV+ffrf+KDSU/j9x01DJu37siTL35CkxZHxf2enZt57vG7+H7FwkrncLmc3DJ+KLf9zzBEDOZM+4gXJ97L/7029ci9bNm2h//763vccN0Q7uxyW3Dg3lWQtwEadeHDN17jk08+5r33PqBx48aUbZjLkrIkXn36b/zv//yMz9Pnseb7o+2JHQ4Hd94ygp7d2/Hmu2ksX7WhgmUnDBvUi1tvuLp6EB3aXglEGA6Kuo3B/uwXRI76K2bz/seb8tHE24ChGLbQvNyqthEcIsSrwZYzBpBDaSYmx7XoVbDKF2uLahBKrZtESzK/AmDAsPGnNO7fb/yZd//xRwYOv4GbfvZbouMSWLlkDq888xCP/mwo/5z6PRGRMQA89fAN7N+7k4kvT6F9lz7kbfmabevns3L1Jtq0anxqN5y/G3bMguZV1E/6SrH0aFP+l177hJZNG/LEo3dSLz6GHbv38+6/v+LdD6Yxf9FqVJXHHrqV+vXrcehQPu999DXfzF5Kx3Yt6NyxalvFG9+MkJwtOIuy8Uce5col3UaiM35HRL/f4Ox4baUxDcNoYkCxLYjIEQdiuPhRdRNDGDUKcVVrhfVN00iE8LkrOK5tXcDCjxChghHhrH0OdGDfriOco6aUe3A//379z3TuMYA/vPARrdp1I7F+Cldfcyf3Pvo8Ofv3kP7JpHLTvIzNG1bRJ3UUA4ZdT72khiQkJtCxXQvuuHkEKY2SqvenuiJwRQd1C2dYEoblgzXvVg0eoHTBm3hb9arAvQ1+ee942rZuSr34GC7v0pZrR6diWRY7d+3n4ftvpGP7liTEx9C2dVN+cstIADZs2nHC5/cktMRZuA/TW9nyLu04jKKl/6As/deV/o8OIVHAMgC1OdxUKUIchKmSW9P166vlQCHQzAqwq6o2aE7FayvhCAFTKivRm9evpLCgavHZoWtf3CGhJ/+gDwXjSOERNV+jYsGsLwj4fYy96b7jGjj1GzSWF568mxWLZ3PTzx7DHRJKvaSGLFvwNRu//462HXsQ3WIkUc2vriA6qp4aZ0QykSkDgs7b2Ja4NqRBm+qtK19+FoEmnaA81tynZwdMs7LTvmmToButY4cWREVWroJt0ji4Lyf35J15S5M7EbFrGSWNuhHAYMOPh1sENcbx4zpcv++Aq3Efml3zJIn1U7AhXyBehAbl5lmEKpG2VVnnPS0AWULjECtv17WvL+lbwVG27fN7Lt9nu/HiI1SEgmJ/5YDb688/yqqlc6v2qWTuwR1ycle+OzRowXg8Nc+N2b75ewAyv5nCymOvX95S7mD2UX3/saff5Y8PXscvbuhF9yuu4ppbHqD3gJHVKttV0q6FMO4PIFWPsXYtx4qt7GONizv+o3A5g6HF+Ljoavf5fTVgCCKUpHQnbPdKchI68uyL71dx0Dp+tmYOYy9vazuTPi8JEBKvQtiQyRodUCIwiEaDRRKnDaDxk9U8oCSG5OwstrXCel6BwEPA37DwGEKIQnGBl5KKY+979PlqOVBMXM0C9vEJwQ8ia+cW2nXuXaMxJcVBt8WeHT/icLqO239YpB2my6+4io9mb+fLj17ji/+8wuO/GENy4xb87Jd/YOCQCgqrv/AwKwGrFMww8JU/ckKLKsGjGrQ5yn5cgKt/ZSMgLKz6SLzbdea54Go4KKvfjthD2/jdI7dX7UisH493/4oAFqWYYChoBA0JEKmKK8lzhhzoUCj1DYtCQ72eSvJI9T7uAAAcs0lEQVTLMN0A3jJKQkJwKvgOeCrrQK3adTvjSbisU1BnWL5oJleNubVGY0LDgr6YXz/1Zo1BFxUTz+33/YEb73qUOdM+4r1X/5enHr2TggfuZMyoq4IvpCi4WLPmr0FLeyFGLKyfctQlV5UIzi8od7ien2YStjsCQsLpGhGCP6rqKJN/t9oBMyQfQAUNWLRB8QvsqYkH+oRKtKUkq3k8Cv3uyDYAFbzSgd3FlTlQbVCv/sOJiIwhc8YU9uz4sWYe4zbBvgFbNpy8jl3L9mNnpR/ZnKUbGX7tT3jri1XExsaTPm1ONcrZLvjuFUJbBRXmsrKqDdD9+4N2h7dZV84XeeOa4irciwSqjnN7cdtCeThDUVFaiEGhBbtP5TpGNa7uJNNgb8DJdgzjobLolF2eqMb7PZH1Y4+8BMEvin9HIZ7afviQ0HBuued/8Hk9PPHANWTvP/kz9R96HS53CFP+9SIlRSdxjqsfAkVHNrWCQIiMiiU2Pg6vt4oPsPAAbJsNbYfQoGEwprV+/brjDtuzZw/r1v0Q5ATOMM4nlTTsQtjeqptY+CUkoFBmH33pTRQKbJMdp3KNKkSYClCvwCZn7s97lwJ/S52q9YGxCK0Hz9akOUPkAEoZQiCrBJ+t+AzBVZsPP+Env2b75h+YmfY+d4y4jGHjbqdj9yuJiUugMP8Qe3dvY/2apTz+l38TFhFFbHwS9/32r/ztTw9w3w29uO62B2nWqgM+n5f9WTv4buEMBg6/gdSrx+PzennoV39gcGpfWrZqRr1GLgqzljJr6gds27KZGydU4bwsOQBNgv9HR0fTtm1bVq5cwWuvvsKgQYMxTIMtW7bw/vvvUS86nAO5daD7qxhHfESehMql7x4jrAwJ5gWVM41WwIGFoyTvjADU7ytibMW3onKNWBowFsUIlDECeBehQG0MW9BiP9ujXLWbUG8YBr975l907TWQj97+C2kfvU7aR5XTOhs1bY1UsJrG3Xw/kVGxvPni73n5qcptkBs1acX1tz9UrsqZmIbJa29+cEThBQiLiOLGW+7gJzcNOmK9caDcYxxbucnmH574I//71ESmfDqZKZ8GewS53W5uueU2YvbM48WZdaN9cCC8Hq6CfRj+MmxnuQvFtvFoaAlKQARbFRRaiLLklDF6nChI11Zi03TeWJl15M/gSsqzUeqrsH3eKEYN+pqfaYBfAX9/ewBjm0cx4mxOxIG9O8netxufz0N4RDRJyY2JjU+qNqSxa9tGDmZn4XaHEp+YTINGFXo9eA9hF/xAXl4e+/fvpyzgJr5xD5JTmuMq24QWbgC/B9kwC1+9JnhMN6EhLpxRrZBjurVm52Szf98+DMOgZctWuAp3cHDFFPIbdiM0xH3ELWAFLDw+HyEuF6ajsh/Itm3KPF6cDgeuKiyxktIyTNMkxH2aTF6V8N0rKGkcjIu5c7ayKtBz2rPuB5bbwo0i5KnSQU2enDdSXjojDuSAWNsmp9KfE8WWqZqu8HNRml35FT38Sq4JLgH2lLCn+dldl5ak5CYkJdesT6eI0KTFZZViX5Xt5XiMxAHEJ0J8m2PVo0goLkN2L0VbX4lTjKMJ4MbxZVKJCYkkJhwNHxTN/BB/2/6EH+NPMh0m4Y7QarltxWDqsXSifTWcELzxzQk5uAVPvZaY2dtZ0+WlRfYBTFWs8pV+wsXmu1OWFMfzPGJwHx8H8RscScI1YUKozR4Izu2P+ccA7kIlVWT7txhlHqTL7RjRHTGi2h/ZxBV3EnkRwG95Tzvr8OyKsjgMXyli+UENlhfHFyD4xcCylVhAAhXr/U4XQGoSQzHH+c0XjpJtovxQPs9XWW7KhKDivDKXgxc8ePK2w/w/Q0QENDq9VbK9S94l0KRznX3EsqTLCD2wAcsR488uDdaGiaKGEIFScKoKdNUcSAjJnCDF1WhMn5YrTk4rQH8luObFxlxK/TYH6iZXsYJme8XN9lfiOqz5ADZ8HIxphcWd/gs6sKVax12dmArTiRHwUhKSeAjAEMQGp624BXaezjkr6UB9JmsoUn0idZNIpu4o5GGEaJQbFQTBKYo/q5Q1TSMqt0+rC2QfmAPeg8d9CUaTm2D/mmDFaONOkHRmfdDt7I3YEZF1ntGaB3eR0/d3C9gLaiMGuIFQjFPz/1TJgdyhhItNtWHffw0Uj8Ln5T/jRCgUDXKhTXlcOAu4e4pg2atw4Dto3R9CYs74lKVLP6Ksee86/+iGz0umo9+Kw9IGwaGCoZze+zOOYUdhtp44mdoF/0GwjgyxggX4i7NPzQV+fjRJH7IxA9n1LaR0hKTac135fSV1Unmu9LL9HozQWF2wLxi/tA1iVQgVsLBrloF4QgCpnxCME9cCzRore1FmBwfgsIUWAIv3kxewgy1f6hx5S4LA2TwHbdAYTW5Vq2tkBH6YjlW/RZ3/fiLWz6Kk7xPbivyAQT2xKVGbaIQC+3CZz5kAyHbgtK2TB0fFyRvla6T6MemtIJYN2wtP3ZN5VqlwD/L9VGTHYjS5KZrSHszab59XtnUJ3vp1v7ONWVrA1kajFpfbFilYeERw2UqB2KeXmlwJQJbiCg05OYAyhssmlEUoXrFJsG1aAiw9GDTzz6/WbMGP02HJS/Djl2hKW7RhGzDOXul+wF/3K7udxdmYMY2Zt4eNKPEi+NQMLn0gkG+YnFbspZIV5jBwet01Y2UKb4owGHCZQh+FLd/sIuvWlljCeWi0kLsFts4EXzHUbw3Ngm57Q/X4gI3WommcsxWNiKrzAAr/YSbmvUtyZ83nEAbdVZiPzZ1AEeA9me5bIwDZNnZ5kf1JKXOsLO+fpgtMuA2ob8Bl+0pYv7eEhQ3DGXD2nRoKWcth7zIIlEFYFDTqcLxuU5WuU4tLhHlWfYG3abe6jR7bxjRD2BKoNzcQoCFCGUp9gSjK/T/iP3kjhZMDSE+ty4Zh8leUG9TGVGGw2Pw4fx9Lbmp5lgBUmAU75oEnFwIlENsw6MM5j+TzFJ6wYVRdoMgfpuMc8gdm7eZ7hKYKa4FxCrYhHFIF2zi9CmPHMYDwn8rgzFHyw8CpukMkGEtRgx6Tt7FsQgvKzGCAruZ+lOJC/vPmM1x/x0PBKLuvBPavDXb28hWBVQahkZDQGhwpdefjDvjP+TVLSsswRAgNrRlwnaX5aPsJRenTiUI5WM6B+6NsVvAF3RCnF446Iw5UrrN+KA5+LTYFwIB8H6u3FTC3VQwjTzrY8mPn72LG5NeZ9k0aNwwfTOzWyfCjJyh6YlOgfos6vSylyrnPe845lM/7H37NlX06M6Bf1xNWkkSsm4Gr70MsOsDSgBJX3pV+JMGkhR8JrvlWVLF5xmkDKMR96vnNKnwpysMqWKKENfBuv3X1nPk7WzX2gDME/B4wpDwmZQVbn1gBEGXjtl289cVsOnbuzIv/eAOXy8UFRXYg2KbkHFPTlAY8/ps7mJ3xHU+/8D7Xj02lbeumVeo+7pI86H2/5/W5ZKmyTZQoFa5S2AvkCdRTTj+bohKAfK5TdybNX826Ad0xxcaj4Cp0xF22PqJXcUHigQPRbqPKjK+DB3N4++238Pq8PPbkn0hMTOKCpLI8bIf7vFzaNE2GDelN396d+PyreXw9aym33DCMxHpH0taJWpOGa+izfLKNSftKMQwhW4VxKA4R/oHNAAREawlACTmn4UwKJpvNV8gWoU+ZhhdvKoltvzY/d/OVSVYlZHi9Xj7//FO++24ZP73r57Tv0IELmkwXoud3HdiIiDBuv3E4O3bv45/vp5OcnMD4cYMINxSX3+JAs7EZb82h0BRyVElCuAooiczesrI0KulmAMPyMfbVZSlp9/c85XBUJeE5ZYKc3lIzygwxiRLYEBBHSYFGhHyR3TDcZ+sRkVhaWspvfvMI9eol8MILL1/44AFwhAbFcR2gpikNeOzh22jdIoXnXv4Ac8lH6C2fHvrNEj4HihS8ajAWxaHKJMN3KC0yZ33HyJz1HcNzt/zKwH78jDnQaasCBjMMm6dcLgZ6fHxYRljxtrLo5vP3h/44JNnTFSAsLIyXXvobDoeDi4YcrnO6YMrJSETo3aMD/ep5iHHF2i/vb/O3LA8lhuBTm+YYdANKHPCcIPfXhke1VsLH80fLZoRcr49QgQ9tpMyjoYF/7W3SoCwgR7jQRQWeI2+tjj2T5Sd6/wZm9n7/5em72CJQKoqBwY2AqPDqnLFygFpyp9Za/oHYfK7CNS43L4uw1Svu0nwrLGLStqQcLmIyzbplOcYu/5gdV38064W1rBeLDaJEWwaDgCZAnsPPc0G1o3ZWxqs1ANnCFFEmzBxKqeHnd2JQXCZhJXPzEhM2FYXkXqwAcoZFYQQ8deJewjfN52D9QdkP7u7xmR1gsZg0ESFOlDFBEcfEOdfKoXL/y08Rbj+82arvnhbjqM0HGDBVt4pyQ+ZYWZ46VX+D8qADf2gyObzdY1u0U/SiW4rQ3rqQvD0r8TQ8v0aBqyALtqzz/uKyRY9kl7HaYbLTVkYBDwAdFNYX76PLinukVl3ntZpCZwifCEwASPDwd4QVfpz+AxrreOaH+L0XIwcy4hpheM5zFarlw/xhQeC3bWc/ll3GjvmrWao23QUGAh0ARXmgtsFT6wBSm3+qcD2qMmWC+JzK/aIc9Epo2bKSRrHTdzt3XXQIcoWj53nN+5BvP/M/3+bDZ7Z5Q3cn+Zid2pnGYtBNhWuDXhbemje2Qp+nugqgzLGyBdg5KJ2BEEx/NUx+gxAoMSJL3s5qFbElzz5wUQHI8iFy/taZMb9L832Y9NvJ35pdfzCimD5lPQHD5EoL7kZxAvudnuNXZK6TACrXqt62hZ8f/jl3tMzEYJICeWac70+bmsgh7+nlntRJ/OxajVUv+fxce/nMsvSIW+d9FnfXNKebaZkDxdO3G5fZyoOiNFBQMbhz9gQpuGAAVLSXyar0vHK6Hulnl7mcZwwhExXdYzYO/GFFfHGpD//FACDv3u/xRzc+59ctW5lROM09bsl/6j/ybkD5auYwKRk/WV1OYSLC5eWuldcyRsmMs6oD1vYJV9wjfpS3jQBHe9NNFFs93K3CDlsN2eS8zPfsckeWbQes2rru2rVr2Lhx4zl/kb7ifOzTWOe+zHP6edTZa1YcmuMatmZKo0cnRQlpi8ZKEUB2KA8pXFeu92wsOsDDZ92IOCtKnZtJAjdXXMg+c4IUm8J4TA7aKq5Fzl72C99ae+zAmceSLMvinbffokGDBufWhM/egu069Wi8z+fnr3//D5Z1at9PwMa3eeWGnMVmv3UfN/r9ixrJ1PTyPk4D0vQqUZ4SRRQKHQHGng2r65wAaMbVkoswb0A6lfrzzx0tWU7hdoFSxQj5xhjsfW25fxue/DO63tSpafTvn0p0dPQ5BVDp/Dcoa33lqRtuLidX9OzEzLnLajymoDSQvXbljqIVoQMXTUn+7Z9ZwVeH89f7fa6txWAK4JbgOia/mnOt/Hgu5uBsllI+K3DPsX/OGimrDbhPwW8bEv65MdT35vch68nPOq2LFBUXkZExl7Hjxp1T8HhWTMET3+i0q1EH9u/G8lUbKCg86ZpubM4tW7dh0yH34vjx/5ne4J5n5o1ldubEYCf5Iena2OFkHuUl5qq8lTma986ZH+xsnbh8OfGN/dP0uFalc8bKLIFfiRBQCP9ErpKXdzRfq1lrT/k67/7zHW699bZzGqgN7FlL2a4VeFNOP6HfMAyuGzOQT9Oqd8+U+a38RT8WrTmwsyw5rcGDf1ocM+LluaNl2eGVI/t/qSkWZKDlK0Yq0ywvTx67OM6FyoEwAzxvGFS5IlzGGEkTm0fFwFYlPM3f0/nnwnGL7I2ztFL7lRPQzp07OZhzkJ49e5078Gz/luIl/6K045k3ImnXthmlpR6276jspLdtrHW59sqlPxQV+sqckW83ffHnux1t35k/RrYfPmbQNG1imGSoBlfaQfhWhf9dMEHOafD6rAKoPHC3cUi6Vmnnzh0rU0T5vSo2Sujc4oZxD5uPf21tXW6Rf/LkuDfffIOf3333uRNbyz+maE0aJV1H1do5b7huCB9/NvtIs88DpfbuqdtZkLczv9n+sHbfvdXomdtiPeaXmdfIEUUxNU1bWhbz0WBfAhFWAC8NHM2Kc22Fnv12EhFMsqBaBWXuGPnIhl8J+FBC1+a7m93i/W1GkdYrY8vCajP+Fi1cSEpKCikp58AH4ymkOO1xior2U9phSK2eOrFeLC2aN2TOoh8KZ+1zL1i12euLLyxoNy3p/semNfj5A/PHyeKKmaID07UzQqbA4QdfhfKmGcrsiXLuS0TOOoAyB0pALdb3n6rNqjtm/liZasC9KhQCoQfKaHjzrqHL97W/fze7VkP2psos3rL5ZPLH3HbrHWfZTrcpm/c6+V89RVGrK/Cl1H4RY4HPygnrdOWCz6cvEndWVodiZ8N1k5q8ONIjrf+ZOVL2Vzx2QLqOVmURBNf3UlilyltqsHjOEDkvnVHOWVHKoHQdOne0zDzRMVdN0y5+i5eBpkCpQyj+cw+sXrqmN5u/hqRmEBUMGxQVFRIZeZZq0gMBPIvexHNgC77m3fDH1H6oIqfUv2NNvnv7ruJwX8ui3d0LA5L3edNnfl0a4l5YVa/C1DR9AOFljvYdWGQoHwSUXfPHyXTOE50zAPX7SmPDHciMq+WEyWX9v9QUw+Q5Va4QKBUDe0wT9jzYniHG1pkG+5YHmydE1n4pkL1vPSXffYzlK8XXvDv7AyEsWfY9vXt0oF78mXcxC9h4txbYm1YXhO4t8zkdzUqyOoEZWJhw3Z/WhvWeumisHJfykpqhIVrMq6IVjBFlBsIXKCVeL5+dSlFgaoY6arqYXE3onNm+C0dJ3vjJWmX+55h3FkY6vI72AOz9loAz/J+F8a324HCPVgszbTsp3+Uw65UrhnaKbTm0AVtmwJbFENcQ4s6styG+YjxL/40nZwd2eCRlbfsf8e1kb9rBx5/NplnT5NMGkCrW/hJrx8bikH1bSiMKwwKe8JSifZfbprNsfv2b/7A19PJvZo+WKtNcUqdrI4r5VJTDZmZAYYohZKpgYzJ7ydiagWdwmib5DaJa/8iOzFp8r+c0I7y6ZYRMj6OzLSw4opj5S4jet/bG/KY9VorFL4CkvcU0vmE2ux/qyLYRLYf1peUw2L0EdiwNVrymdIGaFvl5CvAseR9P3l7UBF/TbgSSa69BlKX495Xo7m3FZtbW0vACn7poWpLVpo0/p63PdOdNS7r/vqzolkszR8ieah2N6TpCA7xHsPQYIB+YZMA2ANNk8ZwRlXWkqmjILI32lzLUstgwf6z8ML+W32mdLZPwR8Z3tn18aTh4DpvrxaCn3yby+TXw2TamPd+bLnEpfRqS0ge8hbDhCyjNgZAQqN8ODOdxoClb8SmeQ3uQQAne5t0JpLSvHdGH2HlecvaUsD+r1HUoyxtaamGS5DnUsLlvTxtsISe0xcz3Gv7idVc462ZfVX16xeh0DSuGv6pybwUV4wdbeMPUYAaDCj/OGSHrT3RPfdM00qFcHyjFl+hlyqmsAXZRAAgcsQ6TLuWlS+sUTJT2IsRuK6TthNnk3NqKjbe3YaDhjjLoUm6RFe2DrTOgLBdKs/FsX4bH50UdDjzNe2AnNTuju1I0kO+xcw55yD3oMwtyvK6SA4FwX8A2NNJfGlnfm92mtWW7VIxAoTNh3fuNfvc/3lD3xtSr2TX/JGZ2appeXqR8AEcXrhF4F+Uzs9zywmB/QulRbn0s9flG49w+bhchUX28lnld9VzuogaQov4Kqn6+WCzGIFuVjgItLZuk9zaR9J8trGsayfxeieQkhRKdFNogLqb5nfERDmJCivbEmXl/TjH3rQqhLM8RtSZN1B2Jr34ryhLaQIXFb20lYNm212er12cZPo8l3l35PoAmK/d5v98R4czO94f4i2xHAAxifYVxcb6CBlF2QXgUOYBh+c3wQ6tjRr65OnrgIr+wvSyL3SuuDUbET6R3jE7XsEL4I8ojFd5JEfCgwkaRYCs6G4p8pcyqqoI4dZrWx+ZOfLQ0/Lw29zpZeVFZYSei0ZOW1zNt/6BKFgu6KD+lT6nbR4K6qKc2CRjEi5KoNpeVL5DWHA1OuAibRchQpdrkfbeBo1PJ0uZtczMuD/dnNwPbYath2mKgqBjYpoUREMBU2xlQtfw+P06XW8UwQMRGUTACZWbk/p1hnZYtix620usI2Scm+6wSshZM4ODhWFWN3Btp2t8W3qzIdYCltnC7IbjF5opyn48vYJBWycRXldR0eqlwk0CiafPmnLOU+1ynAVRjmqjGkL5EahFJASd9gS5i0QKDThWBhDAPpdZZt2VTbAi5ouRZJrlGgJyKIYZT9Is1tJVngVsqvAePKk8lennuQAiNRRhiBPN7LL/y9WEzv2+aRroMRqvNzQqHDHgzY6wsuqj9QGeDUtO1nkIfURJtpZEIYwwlimAJ7zaE+Q5haQAQxYXitPT4BqCGgS02fgVLwYuBR208hkEZJsV4KU7wU3TazSeOEVdFyi+Bx4HIoyKb+Wpw7/xRsqEcXFcLmLagljCnQSk7sx30FAdjUQYgLMXkvcyRsvp8voMLGkAVnJTNHdBThDK1SVEYLkqz8jdzUJRP1c3UzOGy43zd4/jJaua4uRXhT0DFHn0HUB7LHMP7iOjgNE0KGIwwFKcIhloUqtBcDIYDWSpM9VtH01jPN10UADos3gZ3oqXlohs2xZaB31AGiTIICC8/ar1tMN1t8c2sKry+Z8caUBkwletFeApoW2Hm/aq87vTw5OwJUjDutaWfggxGxGUbpq+wfrflGNRX2CU2MwMB0hdeJ9vq2rRfPACq8KUfDKGtDZ1EyTNsNuOghaVcDfSVcrGhsFWERYbB4vhYvptyxen1CDwRoFO7cy3wJErHSgYmfI7yu8yxsiU1XeuJ0D1i75pXzICnZdDPYwbyG17+tGnyz7kjZWddnu+LDkCVXmBXmovQWW0cOFiXEMfWg7m0U5srUS4H2iO4AQtlsxj8gPKDBljvz2Pnop+eupgYPl3dHosbVPl9JY4TnO2F2HyCAdi0UaG1QJwq66MOrOlpBjzlx0v+l7/oFXshTPPFC6BjfCRi015tGmGwE5MtmcPJGj8F5yGTDrabdli0EYM2ttJS4HD/3HxVdovBblVyRSgA8jEoED2mrk2IV4sxwIiKynE5y8lFOSjCBhV2is02w8kGw2Lj7BXsYaLY415dOrO8/RzAwS9/0TvhEoDqGPWZrKHucNqIRWtVXLayzSlsnTOW7Iq+m9R0rYdBIywa2TZxhhArQpxClIILKQeYIgINENqI0kQr5FepoALLDOVV22RW5ggOnChXeeJENTLJNAAS2uXolAkTrEsAqsM0+HONVyctLKWFYeNUk10OYVdsKXtOFjfqP1WbGcItwJ2H00orzGgByrsor5b3Crio6b8WQMeCyXKTIgEam96iNgaBAsPvP2ja3pyQ0vwcH77S0oTuJQGTCRIEzhXHzp3CWhFep4wPql1z9hKALn4a9/rS7ShNK4HDcPgLGnYHxan/397ZrBAQhWH4+YapmbId1zNrkWtwZRZWykaU7ZDs5D4kZYjDjDk2FCMbNSLfs/yWb0/nrdP5uQ9N2FhLV4RO1JDpP+ZVVmXy+zbPT5daEfc2dSwnHBYIszRhUiqzKmXE11rbBgfioo5OqEA/rJUVBlh6vkt/VJMYIGxbLw3wzymeuHhZQrCuUA2H1uGMcWG/O2JOPmZe5/DJC38q0Df1fJYsxw1p5udRSwxg/jkbR/V4r9YUXYFeEcPj52sCG41FUQrgAn/P0GGexEAgAAAAAElFTkSuQmCC"> +$else$ +<img height="144" width="144" src="$url$/img/logo_128px.png"> +$endif$ +>> \ No newline at end of file diff --git a/duniter4j-es-subscription/src/main/resources/templates/css.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/css.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/css.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/css.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/css_logo.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/css_logo.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/css_logo.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/css_logo.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/event_item.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/event_item.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/event_item.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/event_item.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/html.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/html.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/html_email_content.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html_email_content.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/html_email_content.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html_email_content.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/html_event_item.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html_event_item.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/html_event_item.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/html_event_item.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/i18n.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/i18n.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/i18n.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/i18n.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/i18n_args.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/i18n_args.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/i18n_args.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/i18n_args.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/text_email.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/text_email.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/text_email.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/text_email.st diff --git a/duniter4j-es-subscription/src/main/resources/templates/text_event_item.st b/duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/text_event_item.st similarity index 100% rename from duniter4j-es-subscription/src/main/resources/templates/text_event_item.st rename to duniter4j-es-subscription/src/main/resources/org/duniter/elasticsearch/subscription/templates/text_event_item.st diff --git a/duniter4j-es-subscription/src/main/resources/plugin-security.policy b/duniter4j-es-subscription/src/main/resources/plugin-security.policy index 3ea59400b9bd2a3d0e1b89091169fa50b60e1123..ea5437f6726c44547a1dce7470385fc69c055e22 100644 --- a/duniter4j-es-subscription/src/main/resources/plugin-security.policy +++ b/duniter4j-es-subscription/src/main/resources/plugin-security.policy @@ -1,4 +1,4 @@ -grant codeBase "file:${es.path.home}/plugins/duniter4j-es-gchange/"{ +grant codeBase "file:${es.path.home}/plugins/duniter4j-es-subscription/"{ permission java.io.FilePermission "/etc/ld.so.conf", "read"; permission java.io.FilePermission "/etc/ld.so.conf.d/*.conf", "read"; permission java.io.FilePermission "/usr/local/lib/*", "read"; diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginSettings.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginSettings.java index 00601581814e13aeab27fcdb714404e86418a8f7..e640a389057facdda36a00115d2cbe8fbc794f7a 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginSettings.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginSettings.java @@ -212,8 +212,17 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> { return this.nodePubkey; } + public String getCesiumUrl() { + return this.settings.get("duniter.share.cesium.url", "https://g1.duniter.fr"); + } + public String getShareSiteName() { + return this.settings.get("duniter.user.share.site.name", "Cesium"); + } + public String getBaseUrl() { + return settings.get("duniter.share.base.url"); + } /* -- protected methods -- */ diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/Attachment.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/Attachment.java new file mode 100644 index 0000000000000000000000000000000000000000..3073c805f247a62e2eb6a0c1c8d2f62cda28178b --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/Attachment.java @@ -0,0 +1,63 @@ +package org.duniter.elasticsearch.user.model; + +/* + * #%L + * Duniter4j :: Core Client API + * %% + * Copyright (C) 2014 - 2016 EIS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-3.0.html>. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonSetter; +import org.duniter.core.client.model.elasticsearch.Record; + +/** + * Created by blavenie on 01/03/16. + */ +public class Attachment extends Record { + + public static final String JSON_PROPERTY_CONTENT_TYPE = "_content_type"; + public static final String JSON_PROPERTY_CONTENT = "_content"; + + public static final String PROPERTY_CONTENT_TYPE = "contentType"; + public static final String PROPERTY_CONTENT = "content"; + + private String contentType; + + private String content; + + @JsonSetter(JSON_PROPERTY_CONTENT_TYPE) + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @JsonGetter(JSON_PROPERTY_CONTENT_TYPE) + public String getContentType() { + return contentType; + } + + @JsonGetter(JSON_PROPERTY_CONTENT) + public String getContent() { + return content; + } + + @JsonSetter(JSON_PROPERTY_CONTENT) + public void setContent(String content) { + this.content = content; + } +} diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java index 6ef6ecb0cc217f821fb3399e98370fbc4f8f8371..99c94b55a8ae38bad94d70b3ec0ea852565ec625 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserProfile.java @@ -43,6 +43,7 @@ public class UserProfile extends Record { private String locale; private String address; private String city; + private Attachment avatar; public String getTitle() { return title; @@ -91,4 +92,12 @@ public class UserProfile extends Record { public void setCity(String city) { this.city = city; } + + public Attachment getAvatar() { + return avatar; + } + + public void setAvatar(Attachment avatar) { + this.avatar = avatar; + } } diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/RestModule.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/RestModule.java index 1422b77c0aac986165ae788fa60932df524ed010..fd91b23d0607055b152d9848d1e520c408abadba 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/RestModule.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/RestModule.java @@ -50,6 +50,7 @@ public class RestModule extends AbstractModule implements Module { bind(RestUserEventMarkAsReadAction.class).asEagerSingleton(); bind(RestUserEventSearchAction.class).asEagerSingleton(); bind(RestUserAvatarAction.class).asEagerSingleton(); + bind(RestUserShareLinkAction.class).asEagerSingleton(); // Group bind(RestGroupIndexAction.class).asEagerSingleton(); @@ -76,6 +77,7 @@ public class RestModule extends AbstractModule implements Module { bind(RestPageCommentUpdateAction.class).asEagerSingleton(); bind(RestPageCategoryAction.class).asEagerSingleton(); bind(RestPageImageAction.class).asEagerSingleton(); + bind(RestPageShareLinkAction.class).asEagerSingleton(); // Mixed search bind(RestMixedSearchAction.class).asEagerSingleton(); diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/page/RestPageShareLinkAction.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/page/RestPageShareLinkAction.java new file mode 100644 index 0000000000000000000000000000000000000000..ecd1c817499ab22f60f370ff4ba7e231ba7da755 --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/page/RestPageShareLinkAction.java @@ -0,0 +1,108 @@ +package org.duniter.elasticsearch.user.rest.page; + +import com.google.common.html.HtmlEscapers; +import org.duniter.core.exception.BusinessException; +import org.duniter.core.exception.TechnicalException; +import org.duniter.core.util.StringUtils; +import org.duniter.elasticsearch.exception.DuniterElasticsearchException; +import org.duniter.elasticsearch.rest.attachment.RestImageAttachmentAction; +import org.duniter.elasticsearch.rest.share.AbstractRestShareLinkAction; +import org.duniter.elasticsearch.user.PluginSettings; +import org.duniter.elasticsearch.user.dao.page.PageIndexDao; +import org.duniter.elasticsearch.user.dao.page.PageRecordDao; +import org.duniter.elasticsearch.user.model.UserProfile; +import org.duniter.elasticsearch.user.model.page.RegistryRecord; +import org.duniter.elasticsearch.user.service.PageService; +import org.duniter.elasticsearch.user.service.UserService; +import org.duniter.elasticsearch.util.opengraph.OGData; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestController; +import org.nuiton.i18n.I18n; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Locale; + +public class RestPageShareLinkAction extends AbstractRestShareLinkAction { + + @Inject + public RestPageShareLinkAction(final Settings settings, final RestController controller, final Client client, + final PluginSettings pluginSettings, + final PageService service) { + super(settings, controller, client, PageIndexDao.INDEX, PageRecordDao.TYPE, + createResolver(pluginSettings, service)); + } + + protected static OGDataResolver createResolver( + final PluginSettings pluginSettings, + final PageService service) throws DuniterElasticsearchException, BusinessException { + + return (id) -> { + try { + RegistryRecord record = service.getPageForSharing(id); + + OGData data = new OGData(); + + if (record != null) { + + // og:title + if (StringUtils.isNotBlank(record.getTitle())) { + data.title = record.getTitle(); + } + else { + data.title = pluginSettings.getShareSiteName(); + } + + // og:description + data.description = HtmlEscapers.htmlEscaper().escape(record.getDescription()); + + // og:image + if (record.getThumbnail() != null && StringUtils.isNotBlank(record.getThumbnail().get("_content_type"))) { + String baseUrl = pluginSettings.getBaseUrl(); + data.image = StringUtils.isBlank(baseUrl) ? "" : baseUrl; + data.image += RestImageAttachmentAction.computeImageUrl(PageIndexDao.INDEX, PageRecordDao.TYPE, id, RegistryRecord.PROPERTY_THUMBNAIL, record.getThumbnail().get("_content_type")); + } + + // og:url + data.url = String.format("%s/#/app/page/view/%s/%s", + pluginSettings.getCesiumUrl(), + id, + URLEncoder.encode(record.getTitle(), "UTF-8")); + } + else { + + // og:title + data.title = pluginSettings.getShareSiteName(); + + // og:description + data.description = I18n.t("duniter.user.share.description"); + + // og:url + data.url = String.format("%s/#/app/page/view/%s/%s", + pluginSettings.getCesiumUrl(), + id, + ""); + } + + // og:type + data.type = "website"; + + // og:site_name + data.siteName = pluginSettings.getShareSiteName(); + + // default og:image + if (StringUtils.isBlank(data.image)) { + data.image = pluginSettings.getCesiumUrl() + "/img/logo_128px.png"; + data.imageType = "image/png"; + } + + return data; + } + catch(UnsupportedEncodingException e) { + throw new TechnicalException(e); + } + }; + } +} diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/user/RestUserShareLinkAction.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/user/RestUserShareLinkAction.java new file mode 100644 index 0000000000000000000000000000000000000000..540e6e4e0fcdf07c324b0b6a248d078763e7b2c2 --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/user/RestUserShareLinkAction.java @@ -0,0 +1,130 @@ +package org.duniter.elasticsearch.user.rest.user; + +import com.google.common.collect.Maps; +import com.google.common.html.HtmlEscapers; +import org.duniter.core.exception.BusinessException; +import org.duniter.core.exception.TechnicalException; +import org.duniter.core.util.StringUtils; +import org.duniter.elasticsearch.exception.DuniterElasticsearchException; +import org.duniter.elasticsearch.rest.attachment.RestImageAttachmentAction; +import org.duniter.elasticsearch.rest.share.AbstractRestShareLinkAction; +import org.duniter.elasticsearch.user.PluginSettings; +import org.duniter.elasticsearch.user.model.UserProfile; +import org.duniter.elasticsearch.user.service.UserService; +import org.duniter.elasticsearch.util.opengraph.OGData; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestController; +import org.nuiton.i18n.I18n; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Locale; + +public class RestUserShareLinkAction extends AbstractRestShareLinkAction { + + @Inject + public RestUserShareLinkAction(final Settings settings, final RestController controller, final Client client, + final PluginSettings pluginSettings, + final UserService userService) { + super(settings, controller, client, UserService.INDEX, UserService.PROFILE_TYPE, + createResolver(pluginSettings, userService)); + + if (StringUtils.isBlank(pluginSettings.getBaseUrl())) { + log.warn(I18n.t("duniter4j.es.share.error.noBaseUrl", "duniter.share.base.url")); + } + } + + protected static AbstractRestShareLinkAction.OGDataResolver createResolver( + final PluginSettings pluginSettings, + final UserService userService) throws DuniterElasticsearchException, BusinessException { + + return (id) -> { + try { + UserProfile profile = userService.getUserProfileForSharing(id); + + OGData data = new OGData(); + + if (profile != null) { + + // og:locale + Locale locale; + if (StringUtils.isNotBlank(profile.getLocale())) { + locale = new Locale(profile.getLocale()); + data.locale = profile.getLocale(); + } + else { + locale = I18n.getDefaultLocale(); + } + + String pubkey = I18n.l(locale, "duniter.user.share.pubkey", id); + + // og:title + if (StringUtils.isNotBlank(profile.getTitle())) { + data.title = profile.getTitle(); + data.description = pubkey; + } + else { + data.title = pubkey; + data.description = ""; + } + + // og:description + if (StringUtils.isNotBlank(data.description)) data.description += " | "; + if (StringUtils.isNotBlank(profile.getDescription())) { + data.description += HtmlEscapers.htmlEscaper().escape(profile.getDescription()); + } + else { + data.description += I18n.l(locale, "duniter.user.share.description"); + } + + // og:image + if (profile.getAvatar() != null && StringUtils.isNotBlank(profile.getAvatar().getContentType())) { + String baseUrl = pluginSettings.getBaseUrl(); + data.image = StringUtils.isBlank(baseUrl) ? "" : baseUrl; + data.image += RestImageAttachmentAction.computeImageUrl(UserService.INDEX, UserService.PROFILE_TYPE, id, UserProfile.PROPERTY_AVATAR, profile.getAvatar().getContentType()); + } + + // og:url + data.url = String.format("%s/#/app/wot/%s/%s", + pluginSettings.getCesiumUrl(), + id, + URLEncoder.encode(profile.getTitle(), "UTF-8")); + } + else { + + // og:title + String pubkey = I18n.t("duniter.user.share.pubkey", id); + data.title = pubkey; + + // og:description + data.description = I18n.t("duniter.user.share.description"); + + // og:url + data.url = String.format("%s/#/app/wot/%s/%s", + pluginSettings.getCesiumUrl(), + id, + ""); + } + + // og:type + data.type = "website"; + + // og:site_name + data.siteName = pluginSettings.getShareSiteName(); + + // default og:image + if (StringUtils.isBlank(data.image)) { + data.image = pluginSettings.getCesiumUrl() + "/img/logo_128px.png"; + data.imageType = "image/png"; + } + + return data; + } + catch(UnsupportedEncodingException e) { + throw new TechnicalException(e); + } + }; + } +} diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/PageService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/PageService.java index 881fd85ef293b7d62b20afade1c87d375c4d5cd0..f4b4429e5af3cca16913fdff7eaf0fc9742d8c80 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/PageService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/PageService.java @@ -32,6 +32,8 @@ import org.duniter.elasticsearch.user.PluginSettings; import org.duniter.elasticsearch.user.dao.page.PageCommentDao; import org.duniter.elasticsearch.user.dao.page.PageIndexDao; import org.duniter.elasticsearch.user.dao.page.PageRecordDao; +import org.duniter.elasticsearch.user.model.UserProfile; +import org.duniter.elasticsearch.user.model.page.RegistryRecord; import org.elasticsearch.common.inject.Inject; /** @@ -138,6 +140,13 @@ public class PageService extends AbstractService { commentDao.update(id, json); } + public RegistryRecord getPageForSharing(String id) { + + return client.getSourceByIdOrNull(recordDao.getIndex(), recordDao.getType(), id, RegistryRecord.class, + RegistryRecord.PROPERTY_TITLE, + RegistryRecord.PROPERTY_DESCRIPTION, + RegistryRecord.PROPERTY_THUMBNAIL); + } /* -- Internal methods -- */ diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java index 745f5b828e6eb0bfe35efa97b875556329b5c4d9..2d080a88ba9d1309ec607cb8aa9a5345c5103003 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java @@ -24,19 +24,26 @@ package org.duniter.elasticsearch.user.service; import com.fasterxml.jackson.databind.JsonNode; +import org.apache.lucene.queryparser.flexible.core.util.StringUtils; import org.duniter.core.util.Preconditions; import org.apache.commons.collections4.MapUtils; import org.duniter.core.client.model.ModelUtils; -import org.duniter.core.client.model.elasticsearch.UserProfile; +import org.duniter.elasticsearch.user.model.Attachment; +import org.duniter.elasticsearch.user.model.UserProfile; import org.duniter.core.service.CryptoService; import org.duniter.elasticsearch.client.Duniter4jClient; +import org.duniter.elasticsearch.rest.attachment.RestImageAttachmentAction; +import org.duniter.elasticsearch.rest.share.AbstractRestShareLinkAction; import org.duniter.elasticsearch.user.PluginSettings; import org.duniter.elasticsearch.exception.AccessDeniedException; import org.duniter.elasticsearch.service.AbstractService; import org.duniter.elasticsearch.user.dao.profile.UserIndexDao; import org.duniter.elasticsearch.user.dao.profile.UserProfileDao; import org.duniter.elasticsearch.user.dao.profile.UserSettingsDao; +import org.duniter.elasticsearch.util.opengraph.OGData; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestStatus; import java.util.HashMap; import java.util.Map; @@ -226,6 +233,15 @@ public class UserService extends AbstractService { return sb.substring(separator.length()); } + public UserProfile getUserProfileForSharing(String id) { + + return client.getSourceByIdOrNull(INDEX, PROFILE_TYPE, id, UserProfile.class, + UserProfile.PROPERTY_TITLE, + UserProfile.PROPERTY_DESCRIPTION, + UserProfile.PROPERTY_LOCALE, + UserProfile.PROPERTY_AVATAR); + } + /* -- Internal methods -- */ } diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties index 98d218d1ac2674b2a99cc8e0d673c1eb85d6576e..3c47a593117794059ed2628a702289ef4d3798b0 100644 --- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties +++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties @@ -15,4 +15,7 @@ duniter.user.event.NODE_BMA_UP=Duniter node [%1$s\:%2$s] is UP again. duniter.user.event.NODE_STARTED=Your node ES API [%1$s] is UP. duniter.user.event.TX_RECEIVED=You received a payment from %2$s. duniter.user.event.TX_SENT=Your payment to %2$s was executed. +duniter.user.share.description=Follow your "libre money" wallets easily +duniter.user.share.pubkey=Public key\: %1$s +duniter4j.es.share.error.noBaseUrl=Image path are relative for share links (og\:image). /\!\\ Set [%s] in the configuration (recommended). duniter4j.ws.user.open=User [%1$s] connecting with id [%2$s] with locale [%3$s] diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties index e4e31362232b4af585877a5b90a3541bf51c8da1..ec9ace345c37859ec4eebeece40f906bd7d5233d 100644 --- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties +++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties @@ -15,4 +15,7 @@ duniter.user.event.NODE_BMA_UP=Noeud Duniter [%1$s\:%2$s] à nouveau accessible. duniter.user.event.NODE_STARTED=Noeud ES API [%1$s] est démarré. duniter.user.event.TX_RECEIVED=Vous avez recu un paiement de %2$s. duniter.user.event.TX_SENT=Votre paiement à %2$s a bien été executé. +duniter.user.share.description=Suivez vos comptes de monnaie libre en toute simplicité \! +duniter.user.share.pubkey=Clé publique \: %1$s +duniter4j.es.share.error.noBaseUrl=Chemins relatif pour les images, dans les partages (og\:image). /\!\\ Renseignez [%s] dans la configuration (conseillé). duniter4j.ws.user.open=Utilisateur [%1$s] connecté id\=[%2$s] sur la locale [%3$s] diff --git a/pom.xml b/pom.xml index ea42a4859c7081124d9c4a627b95c62efbdd3942..a58713254a8ae40c7e5eecc5a9478632480fd1dd 100644 --- a/pom.xml +++ b/pom.xml @@ -33,8 +33,8 @@ <slf4j.version>1.7.5</slf4j.version> <guava.version>22.0</guava.version> <xml-apis.version>2.0.2</xml-apis.version> - <kalium.version>0.5.0_blavenie</kalium.version> - <jnr-ffi.version>2.1.0</jnr-ffi.version> + <kalium.version>0.6.0_PR64</kalium.version> + <jnr-ffi.version>2.1.7</jnr-ffi.version> <scrypt.version>1.4.0</scrypt.version> <elasticsearch.version>2.4.6</elasticsearch.version> <jna.version>4.2.0</jna.version> diff --git a/src/scripts/start-es-node.sh b/src/scripts/start-es-node.sh new file mode 100755 index 0000000000000000000000000000000000000000..a9be23c2a5700cafb7bcfa08b37ab480d64a0ea4 --- /dev/null +++ b/src/scripts/start-es-node.sh @@ -0,0 +1,16 @@ +#!/bin/bash + + +PID=`ps -efl | grep duniter4j-es | grep g1/lib | awk '/^([0-9]+) ([^\s]) ([a-zA-Z0-9]+) ([0-9]+).*/ {printf "%s", $4}'` + +if [ "$PID" != "" ]; +then + echo "Error: ES node already started!" + exit -1 +else + cd /opt/duniter4j-es-g1/bin + ./elasticsearch -d + echo "ES node started !" + echo "...to follow log: tail -f /opt/duniter4j-es-g1/logs/g1-es-data.log" +fi + diff --git a/src/scripts/stop-es-nodes.sh b/src/scripts/stop-es-nodes.sh new file mode 100755 index 0000000000000000000000000000000000000000..8181a733c99af4acddd1e759dcb3b3effc6cfcca --- /dev/null +++ b/src/scripts/stop-es-nodes.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Get ES PID +PID=`ps -efl | grep duniter4j-es | grep lib | awk '{printf "%s", $4}'` + +if [ "$PID" != "" ]; +then + echo "Stopping ES node running on PID $PID..." + sudo kill -15 $PID + + sleep 5s + + # Check if still alive + PID=`ps -efl | grep duniter4j-es | grep g1/lib | awk '{printf "%s", $4}'` + if [ "$PID" != "" ]; + then + sleep 10s + fi + + PID=`ps -efl | grep duniter4j-es | grep g1/lib | awk '{printf "%s", $4}'` + if [ "$PID" != "" ]; + then + echo "Error: Unable to stop ES node !" + exit -1 + else + echo "ES node stopped" + fi + +else + echo "ES node not running!" +fi + diff --git a/src/scripts/update-es-node.sh b/src/scripts/update-es-node.sh new file mode 100755 index 0000000000000000000000000000000000000000..28702aadeaff634210f4a1ecbd87ac75b1b60860 --- /dev/null +++ b/src/scripts/update-es-node.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +VERSION=$1 +OLD_VERSION=$2 + +if [ "${VERSION}" == "" ]; then + echo "ERROR: Missing version argument !" + echo " " + echo "usage: sudo ./update-es.sh <version> [<old_version>]" + exit +fi +if [ "${OLD_VERSION}" == "" ]; then + OLD_VERSION=`ps -efl | grep duniter4j-es | grep g1/lib | sed -r 's/.*duniter4j-es-([0-9.]+)-g1.*/\1/g'` + if [ "${OLD_VERSION}" == "" ]; then + echo "Error: unable to known previous version" + exit + fi +fi + +READLINK=`which readlink` +if [ -z "$READLINK" ]; then + message "Required tool 'readlink' is missing. Please install before launch \"$0\" file." + exit 1 +fi + +# ------------------------------------------------------------------ +# Ensure BASEDIR points to the directory where the soft is installed. +# ------------------------------------------------------------------ +SCRIPT_LOCATION=$0 +if [ -x "$READLINK" ]; then + while [ -L "$SCRIPT_LOCATION" ]; do + SCRIPT_LOCATION=`"$READLINK" -e "$SCRIPT_LOCATION"` + done +fi + +export BASEDIR=`dirname "$SCRIPT_LOCATION"` +cd $BASEDIR + +echo "--- Downloading duniter4j-es-standalone v$VERSION... ----------------------" + +if [ -f "downloads/duniter4j-es-${VERSION}-standalone.zip" ]; then + echo "...removing file, as it already exists in ./downloads/duniter4j-es-${VERSION}-standalone.zip" + rm ./downloads/duniter4j-es-${VERSION}-standalone.zip +fi + +if [ ! -e "downloads" ]; then + mkdir downloads +fi + +cd downloads +wget -kL https://github.com/duniter/duniter4j/releases/download/duniter4j-${VERSION}/duniter4j-es-${VERSION}-standalone.zip +cd .. + +if [ -f "downloads/duniter4j-es-${VERSION}-standalone.zip" ]; then + echo "" +else + echo "Error: unable to dowlonad this version!" + exit -1 +fi + +echo "--- Installating duniter4j-es v$VERSION... ---------------------" +if [ -d "/opt/duniter4j-es-${VERSION}-g1" ]; then + echo "Error: Already installed in /opt/duniter4j-es-${VERSION}-g1 !" + exit -1 +fi + +unzip -o ./downloads/duniter4j-es-${VERSION}-standalone.zip +mv duniter4j-es-${VERSION} duniter4j-es-${VERSION}-g1 +sudo mv duniter4j-es-${VERSION}-g1 /opt/ +sudo rm /opt/duniter4j-es-g1 +sudo ln -s /opt/duniter4j-es-${VERSION}-g1 /opt/duniter4j-es-g1 + +mkdir /opt/duniter4j-es-${VERSION}-g1/data +mv /opt/duniter4j-es-${VERSION}-g1/config/elasticsearch.yml /opt/duniter4j-es-${VERSION}-g1/config/elasticsearch.yml.ori + +./stop-es-nodes.sh + +if [ "$OLD_VERSION" != "$VERSION" ]; +then + echo "--- Restoring files (data+config) from previous version $OLD_VERSION... ---------------------" + tar -cvf /opt/duniter4j-es-${OLD_VERSION}-g1/data/save.tar.gz /opt/duniter4j-es-${OLD_VERSION}-g1/data/g1-* + mv /opt/duniter4j-es-${OLD_VERSION}-g1/data/g1-* /opt/duniter4j-es-${VERSION}-g1/data + cp /opt/duniter4j-es-${OLD_VERSION}-g1/config/elasticsearch.yml /opt/duniter4j-es-${VERSION}-g1/config +fi + +#./start-es-nodes.sh + +echo "--- Successfully installed duniter4j-es v$VERSION ! -------------" +echo "" +