diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserGroup.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserGroup.java index 4144d10dbc89f19040b6325dc6c35a164a90f7d2..1f3f5126c38cc5acca0993aaac9698198d85694f 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserGroup.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/UserGroup.java @@ -27,24 +27,14 @@ package org.duniter.core.client.model.elasticsearch; */ public class UserGroup extends Record { - public static final String PROPERTY_NAME="name"; public static final String PROPERTY_TITLE="title"; public static final String PROPERTY_DESCRIPTION="description"; public static final String PROPERTY_CREATION_TIME="creationTime"; - private String name; private String title; private String description; private Long creationTime; - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - public String getTitle() { return title; } 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 850b5dddb0a164bb697a700edbb41496b86b8448..6bd2985f692e862be784f1e21e626aebc95fd7ca 100644 --- a/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml +++ b/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml @@ -138,8 +138,8 @@ duniter.keyring.password: def # Enable security, to disable HTTP access to the default ES admin API # -#duniter.security.enable: false -duniter.security.enable: true +duniter.security.enable: false +#duniter.security.enable: true # # Security token prefix (default: 'duniter-') # @@ -153,9 +153,9 @@ duniter.security.enable: true # # Should synchronize data using P2P # -duniter.data.sync.enable: false -#duniter.data.sync.host: data.duniter.fr -#duniter.data.sync.port: 80 +duniter.data.sync.enable: true +duniter.data.sync.host: data.gtest.duniter.fr +duniter.data.sync.port: 80 # ---------------------------------- Duniter4j SMTP server ------------------------- # diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java index 93b705e0e330a158065fb44c3772e513f6931da7..eb115021d5cb3c4d88d681423286a220c97c32bb 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java @@ -237,7 +237,6 @@ public abstract class AbstractService implements Bean { } } - protected String getIssuer(JsonNode actualObj) { return getMandatoryField(actualObj, Record.PROPERTY_ISSUER).asText(); } @@ -259,7 +258,7 @@ public abstract class AbstractService implements Bean { Map<String, Object> fields = getFieldsById(index, type, docId, fieldNames); if (MapUtils.isEmpty(fields)) throw new NotFoundException(String.format("Document [%s/%s/%s] not exists.", index, type, docId)); Arrays.stream(fieldNames).forEach((fieldName) -> { - if (!fields.containsKey(fieldName)) throw new NotFoundException(String.format("Document [%s/%s/%s] should have the madatory field [%s].", index, type, docId, fieldName)); + if (!fields.containsKey(fieldName)) throw new NotFoundException(String.format("Document [%s/%s/%s] should have the mandatory field [%s].", index, type, docId, fieldName)); }); return fields; } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractSynchroService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractSynchroService.java index dca7183e5faa01c612dcf283465eaa0d8d17eeca..1ef7130bf95d048234d75e52c6dabf7f2909ee56 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractSynchroService.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractSynchroService.java @@ -172,6 +172,7 @@ public abstract class AbstractSynchroService extends AbstractService { JsonNode node; try { HttpPost httpPost = new HttpPost(httpService.getPath(peer, fromIndex, fromType, "_search")); + httpPost.setHeader("Content-Type", "application/json;charset=UTF-8"); httpPost.setEntity(new ByteArrayEntity(bos.bytes().array())); if (logger.isDebugEnabled()) { logger.debug(String.format("[%s] [%s/%s] Sending POST request: %s", peer, fromIndex, fromType, new String(bos.bytes().array()))); diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java index 9d05f816ded0c839024ca187c7521bf4dafa7b95..c827707dc7a89585f5f7722b9752688386a1899c 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/PluginInit.java @@ -106,6 +106,9 @@ public class PluginInit extends AbstractLifecycleComponent<PluginInit> { injector.getInstance(GroupService.class) .deleteIndex() .createIndexIfNotExists(); + injector.getInstance(UserInvitationService.class) + .deleteIndex() + .createIndexIfNotExists(); if (logger.isInfoEnabled()) { logger.info("Reloading all Duniter indices... [OK]"); @@ -119,6 +122,7 @@ public class PluginInit extends AbstractLifecycleComponent<PluginInit> { injector.getInstance(UserService.class).createIndexIfNotExists(); injector.getInstance(MessageService.class).createIndexIfNotExists(); injector.getInstance(GroupService.class).createIndexIfNotExists(); + injector.getInstance(UserInvitationService.class).createIndexIfNotExists(); if (logger.isInfoEnabled()) { logger.info("Checking Duniter indices... [OK]"); diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java index 8ff7163726074deb8b743007a0e085a7a9dbc7dd..db84a9a7cf1125f1047e0ff32e7542f47af8e7dc 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java @@ -45,6 +45,8 @@ public enum UserEventCodes { CERT_RECEIVED, // Message - MESSAGE_RECEIVED + MESSAGE_RECEIVED, + // Invitation + INVITATION_TO_CERTIFY } 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 fc6d2dede07a396bc56a7cc4d093d7f25f8fddb0..db6d83026c6fa5e333cf08f1c35fbedbd426cde5 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 @@ -25,6 +25,8 @@ package org.duniter.elasticsearch.user.rest; import org.duniter.elasticsearch.user.rest.group.RestGroupIndexAction; import org.duniter.elasticsearch.user.rest.group.RestGroupUpdateAction; import org.duniter.elasticsearch.user.rest.history.RestHistoryDeleteIndexAction; +import org.duniter.elasticsearch.user.rest.invitation.RestInvitationCertificationIndexAction; +import org.duniter.elasticsearch.user.rest.invitation.RestInvitationCertificationMarkAsReadAction; import org.duniter.elasticsearch.user.rest.message.RestMessageInboxIndexAction; import org.duniter.elasticsearch.user.rest.message.RestMessageInboxMarkAsReadAction; import org.duniter.elasticsearch.user.rest.message.RestMessageOutboxIndexAction; @@ -61,6 +63,10 @@ public class RestModule extends AbstractModule implements Module { bind(RestMessageOutboxIndexAction.class).asEagerSingleton(); bind(RestMessageInboxMarkAsReadAction.class).asEagerSingleton(); + // Invitation + bind(RestInvitationCertificationIndexAction.class).asEagerSingleton(); + bind(RestInvitationCertificationMarkAsReadAction.class).asEagerSingleton(); + // Backward compatibility { // message/record diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationIndexAction.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationIndexAction.java new file mode 100644 index 0000000000000000000000000000000000000000..b33c7af9b68cb8fbffd571af6e66a6ca31c50ccd --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationIndexAction.java @@ -0,0 +1,44 @@ +package org.duniter.elasticsearch.user.rest.invitation; + +/* + * #%L + * duniter4j-elasticsearch-plugin + * %% + * 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 org.duniter.elasticsearch.rest.AbstractRestPostIndexAction; +import org.duniter.elasticsearch.rest.security.RestSecurityController; +import org.duniter.elasticsearch.user.service.UserInvitationService; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestController; + +public class RestInvitationCertificationIndexAction extends AbstractRestPostIndexAction { + + @Inject + public RestInvitationCertificationIndexAction(Settings settings, RestController controller, Client client, + RestSecurityController securityController, + final UserInvitationService service) { + super(settings, controller, client, securityController, + UserInvitationService.INDEX, + UserInvitationService.CERTIFICATION_TYPE, + json -> service.indexCertificationInvitationFromJson(json)); + } +} \ No newline at end of file diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationMarkAsReadAction.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationMarkAsReadAction.java new file mode 100644 index 0000000000000000000000000000000000000000..6f65826ff389ca45bb62d64c312c3d788d147fc8 --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/rest/invitation/RestInvitationCertificationMarkAsReadAction.java @@ -0,0 +1,44 @@ +package org.duniter.elasticsearch.user.rest.invitation; + +/* + * #%L + * duniter4j-elasticsearch-plugin + * %% + * 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 org.duniter.elasticsearch.rest.AbstractRestPostMarkAsReadAction; +import org.duniter.elasticsearch.rest.security.RestSecurityController; +import org.duniter.elasticsearch.user.service.UserInvitationService; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestController; + +public class RestInvitationCertificationMarkAsReadAction extends AbstractRestPostMarkAsReadAction { + + @Inject + public RestInvitationCertificationMarkAsReadAction(Settings settings, RestController controller, Client client, + RestSecurityController securityController, + UserInvitationService service) { + super(settings, controller, client, securityController, UserInvitationService.INDEX, UserInvitationService.CERTIFICATION_TYPE, + (id, signature) -> { + service.markInvitationAsRead(id, signature); + }); + } +} \ No newline at end of file diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/GroupService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/GroupService.java index a1f5f5422b61ee114ecf0fa81ebe1bb548745836..8a06c09d9893234844ed4cff6456e32c1f851330 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/GroupService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/GroupService.java @@ -115,15 +115,17 @@ public class GroupService extends AbstractService { public String indexRecordProfileFromJson(String profileJson) { JsonNode actualObj = readAndVerifyIssuerSignature(profileJson); - String name = getName(actualObj); + String title = getTitle(actualObj); + String id = computeIdFromTitle(title); + String issuer = getIssuer(actualObj); if (logger.isDebugEnabled()) { - logger.debug(String.format("Indexing a user profile from issuer [%s]", name.substring(0, 8))); + logger.debug(String.format("Indexing group [%s] from issuer [%s]", id, issuer.substring(0, 8))); } IndexResponse response = client.prepareIndex(INDEX, RECORD_TYPE) .setSource(profileJson) - .setId(name) // always use the name as id + .setId(id) .setRefresh(false) .execute().actionGet(); return response.getId(); @@ -133,39 +135,29 @@ public class GroupService extends AbstractService { * Update a record * @param recordJson */ - public ListenableActionFuture<UpdateResponse> updateRecordFromJson(String recordJson, String id) { + public ListenableActionFuture<UpdateResponse> updateRecordFromJson(String id, String recordJson) { JsonNode actualObj = readAndVerifyIssuerSignature(recordJson); - String name = getName(actualObj); - if (!Objects.equals(name, id)) { - throw new AccessDeniedException(String.format("Could not update this document: not issuer.")); - } if (logger.isDebugEnabled()) { - logger.debug(String.format("Updating a group from name [%s]", name)); + logger.debug(String.format("Updating group [%s]", id)); } - return client.prepareUpdate(INDEX, RECORD_TYPE, name) + return client.prepareUpdate(INDEX, RECORD_TYPE, id) .setDoc(recordJson) .execute(); } + public String getTitleById(String id) { - - protected String getName(JsonNode actualObj) { - return getMandatoryField(actualObj, UserGroup.PROPERTY_NAME).asText(); - } - - public String getTitleByName(String name) { - - Object title = getFieldById(INDEX, RECORD_TYPE, name, UserGroup.PROPERTY_NAME); + Object title = getFieldById(INDEX, RECORD_TYPE, id, UserGroup.PROPERTY_TITLE); if (title == null) return null; return title.toString(); } - public Map<String, String> getTitlesByNames(Set<String> names) { + public Map<String, String> getTitlesByNames(Set<String> ids) { - Map<String, Object> titles = getFieldByIds(INDEX, RECORD_TYPE, names, UserGroup.PROPERTY_NAME); + Map<String, Object> titles = getFieldByIds(INDEX, RECORD_TYPE, ids, UserGroup.PROPERTY_TITLE); if (MapUtils.isEmpty(titles)) return null; Map<String, String> result = new HashMap<>(); titles.entrySet().stream().forEach((entry) -> result.put(entry.getKey(), entry.getValue().toString())); @@ -175,6 +167,29 @@ public class GroupService extends AbstractService { /* -- Internal methods -- */ + protected String getTitle(JsonNode actualObj) { + return getMandatoryField(actualObj, UserGroup.PROPERTY_TITLE).asText(); + } + + protected String computeIdFromTitle(String title) { + return computeIdFromTitle(title, 0); + } + + protected String computeIdFromTitle(String title, int counter) { + + String id = title.replaceAll("\\s+", ""); + id = id.replaceAll("[^a-zA−Z_-]+", ""); + if (counter > 0) { + id += "_" + counter; + } + + if (!isDocumentExists(INDEX, RECORD_TYPE, id)) { + return id; + } + + return computeIdFromTitle(title, counter+1); + } + public XContentBuilder createRecordType() { String stringAnalyzer = pluginSettings.getDefaultStringAnalyzer(); @@ -210,6 +225,18 @@ public class GroupService extends AbstractService { .field("index", "not_analyzed") .endObject() + // hash + .startObject("hash") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // signature + .startObject("signature") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + // avatar .startObject("avatar") .field("type", "attachment") diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/HistoryService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/HistoryService.java index beaf8da4ba22dbfe5a86faf51f92d8c4ffcf37e4..8cfb537bb19a119287c8be8f30649e4248205a5f 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/HistoryService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/HistoryService.java @@ -124,10 +124,14 @@ public class HistoryService extends AbstractService { throw new NotFoundException(String.format("Index [%s] not exists.", index)); } - // Special case for message: check if issuer is recipient + // Special case for message: check if deletion issuer is the message recipient if (MessageService.INDEX.equals(index) && MessageService.INBOX_TYPE.equals(type)) { checkSameDocumentField(index, type, id, MessageRecord.PROPERTY_RECIPIENT, issuer); } + // Special case for invitation: check if deletion issuer is the invitation recipient + else if (UserInvitationService.INDEX.equals(index)) { + checkSameDocumentField(index, type, id, MessageRecord.PROPERTY_RECIPIENT, issuer); + } else { // Check document issuer checkSameDocumentIssuer(index, type, id, issuer); diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java index 71dee94fa0b56c1df7626fea7036efc211dc7470..40bb0e1526ce8218c7d4b3b2295c77fcf2db4f5a 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java @@ -218,7 +218,13 @@ public class MessageService extends AbstractService { .field("index", "not_analyzed") .endObject() - // content + // title (encrypted) + .startObject("title") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // content (encrypted) .startObject("content") .field("type", "string") .field("index", "not_analyzed") diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java index c0b55796046aeaed17eb8abc59a2eef29ae2d45d..e1986861b8cfa378e7adcfd6eb8dc2c632bf4393 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/ServiceModule.java @@ -37,6 +37,8 @@ public class ServiceModule extends AbstractModule implements Module { bind(UserEventService.class).asEagerSingleton(); + bind(UserInvitationService.class).asEagerSingleton(); + bind(BlockchainUserEventService.class).asEagerSingleton(); bind(SynchroService.class).asEagerSingleton(); diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/SynchroService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/SynchroService.java index f39c137efde8eca867e5c1780db0d43085e4463d..6d683a46d56d02e670cfc017ae0c54ecd673874b 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/SynchroService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/SynchroService.java @@ -67,6 +67,7 @@ public class SynchroService extends AbstractSynchroService { importUserChanges(result, peer, sinceTime); importMessageChanges(result, peer, sinceTime); importGroupChanges(result, peer, sinceTime); + importInvitationChanges(result, peer, sinceTime); long duration = System.currentTimeMillis() - time; logger.info(String.format("[%s] Synchronizing user data since %s [OK] %s (ins %s ms)", peer.toString(), sinceTime, result.toString(), duration)); @@ -96,4 +97,8 @@ public class SynchroService extends AbstractSynchroService { importChanges(result, peer, GroupService.INDEX, GroupService.RECORD_TYPE, sinceTime); } + protected void importInvitationChanges(SynchroResult result, Peer peer, long sinceTime) { + importChanges(result, peer, UserInvitationService.INDEX, UserInvitationService.CERTIFICATION_TYPE, sinceTime); + } + } diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserInvitationService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserInvitationService.java new file mode 100644 index 0000000000000000000000000000000000000000..aa0548d404536b9a706b451aa4ccb000e30d8c72 --- /dev/null +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserInvitationService.java @@ -0,0 +1,215 @@ +package org.duniter.elasticsearch.user.service; + +/* + * #%L + * UCoin Java Client :: Core API + * %% + * Copyright (C) 2014 - 2015 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.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.duniter.core.client.model.ModelUtils; +import org.duniter.core.exception.TechnicalException; +import org.duniter.core.service.CryptoService; +import org.duniter.elasticsearch.exception.InvalidSignatureException; +import org.duniter.elasticsearch.user.PluginSettings; +import org.duniter.elasticsearch.user.model.Message; +import org.duniter.elasticsearch.user.model.UserEvent; +import org.duniter.elasticsearch.user.model.UserEventCodes; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.update.UpdateRequestBuilder; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.nuiton.i18n.I18n; + +import java.io.IOException; +import java.util.Map; + +/** + * Created by Benoit on 30/03/2015. + */ +public class UserInvitationService extends AbstractService { + + public static final String INDEX = "invitation"; + public static final String CERTIFICATION_TYPE = "certification"; + + + private final UserEventService userEventService; + + @Inject + public UserInvitationService(Client client, PluginSettings settings, + CryptoService cryptoService, UserEventService userEventService) { + super("duniter." + INDEX, client, settings, cryptoService); + this.userEventService = userEventService; + } + + /** + * Delete blockchain index, and all data + * @throws JsonProcessingException + */ + public UserInvitationService deleteIndex() { + deleteIndexIfExists(INDEX); + return this; + } + + public boolean existsIndex() { + return super.existsIndex(INDEX); + } + + /** + * Create index need for blockchain registry, if need + */ + public UserInvitationService createIndexIfNotExists() { + try { + if (!existsIndex(INDEX)) { + createIndex(); + } + } + catch(JsonProcessingException e) { + throw new TechnicalException(String.format("Error while creating index [%s]", INDEX)); + } + + return this; + } + + /** + * Create index need for category registry + * @throws JsonProcessingException + */ + public UserInvitationService createIndex() throws JsonProcessingException { + logger.info(String.format("Creating index [%s/%s]", INDEX, CERTIFICATION_TYPE)); + + CreateIndexRequestBuilder createIndexRequestBuilder = client.admin().indices().prepareCreate(INDEX); + Settings indexSettings = Settings.settingsBuilder() + .put("number_of_shards", 2) + .put("number_of_replicas", 1) + .build(); + createIndexRequestBuilder.setSettings(indexSettings); + createIndexRequestBuilder.addMapping(CERTIFICATION_TYPE, createCertificationType()); + createIndexRequestBuilder.execute().actionGet(); + + return this; + } + + public String indexCertificationInvitationFromJson(String recordJson) { + + JsonNode actualObj = readAndVerifyIssuerSignature(recordJson); + String issuer = getIssuer(actualObj); + String recipient = getMandatoryField(actualObj, Message.PROPERTY_RECIPIENT).asText(); + Long time = getMandatoryField(actualObj, Message.PROPERTY_TIME).asLong(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Indexing a invitation to certify from issuer [%s]", issuer.substring(0, 8))); + } + + IndexResponse response = client.prepareIndex(INDEX, CERTIFICATION_TYPE) + .setSource(recordJson) + .setRefresh(false) + .execute().actionGet(); + + String invitationId = response.getId(); + + // Notify recipient + userEventService.notifyUser(UserEvent.newBuilder(UserEvent.EventType.INFO, UserEventCodes.INVITATION_TO_CERTIFY.name()) + .setRecipient(recipient) + .setMessage(I18n.n("duniter.invitation.cert.received"), issuer, ModelUtils.minifyPubkey(issuer)) + .setTime(time) + .setReference(INDEX, CERTIFICATION_TYPE, invitationId) + .build()); + + return invitationId; + } + + public void markInvitationAsRead(String id, String signature) { + Map<String, Object> fields = getMandatoryFieldsById(INDEX, CERTIFICATION_TYPE, id, Message.PROPERTY_HASH, Message.PROPERTY_RECIPIENT); + String recipient = fields.get(UserEvent.PROPERTY_RECIPIENT).toString(); + String hash = fields.get(UserEvent.PROPERTY_HASH).toString(); + + // Check signature + boolean valid = cryptoService.verify(hash, signature, recipient); + if (!valid) { + throw new InvalidSignatureException("Invalid signature: only the recipient can mark an message as read."); + } + + UpdateRequestBuilder request = client.prepareUpdate(INDEX, CERTIFICATION_TYPE, id) + .setDoc("read_signature", signature); + request.execute(); + } + + /* -- Internal methods -- */ + + public XContentBuilder createCertificationType() { + return createMapping(CERTIFICATION_TYPE); + } + + public XContentBuilder createMapping(String typeName) { + try { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject(typeName) + .startObject("properties") + + // issuer + .startObject("issuer") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // recipient + .startObject("recipient") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // time + .startObject("time") + .field("type", "integer") + .endObject() + + // nonce + .startObject("nonce") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // content (encrypted) + .startObject("content") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + // read_signature + .startObject("read_signature") + .field("type", "string") + .field("index", "not_analyzed") + .endObject() + + .endObject() + .endObject().endObject(); + + return mapping; + } + catch(IOException ioe) { + throw new TechnicalException(String.format("Error while getting mapping for index [%s/%s]: %s", INDEX, CERTIFICATION_TYPE, ioe.getMessage()), ioe); + } + } +} diff --git a/duniter4j-es-user/src/main/misc/curl_test.sh b/duniter4j-es-user/src/main/misc/curl_test.sh index 1c88ff485fa714301462c7224e5b3da1e4c0d5c0..be2865e117420f1ee1608f68d6226b9db2f5e76f 100755 --- a/duniter4j-es-user/src/main/misc/curl_test.sh +++ b/duniter4j-es-user/src/main/misc/curl_test.sh @@ -88,20 +88,34 @@ curl -XPOST "http://127.0.0.1:9200/user/event/_search?pretty" -d' echo "--- GET market pictures content_type--- " -curl -XPOST "http://127.0.0.1:9200/market/record/_search?pretty" -d' +curl -XPOST "http://data.gtest.duniter.fr/user/profile/_search?pretty" -d' { query: { constant_score: { filter: [ - {term: { issuer: "5ocqzyDMMWf1V8bsoNhWb1iNwax1e9M7VTUN6navs8of"}} + {terms: { issuer: ["5ocqzyDMMWf1V8bsoNhWb1iNwax1e9M7VTUN6navs8of"]}} ] } }, - sort : [ - { "time" : {"order" : "desc"}} - ], from: 0, - size: 3, - _source: ["avatar._content_type"] + size: 100, + _source: ["title", "avatar._content_type"] +}' + + +echo "--- GET user event count --- " +curl -XPOST "http://data.gtest.duniter.fr/user/event/_search?pretty" -d' +{ + from: 0, + size: 0, + _source: false }' + +echo "--- GET message count --- " +curl -XPOST "http://data.gtest.duniter.fr/message/record/_search?pretty" -d' +{ + from: 0, + size: 10, + _source: ["nonce"] +}' diff --git a/duniter4j-es-user/src/main/misc/query_many_indices.sh b/duniter4j-es-user/src/main/misc/query_many_indices.sh new file mode 100755 index 0000000000000000000000000000000000000000..22a30bdbb72556b7fd04ce51f5cdf9bd07b25c1c --- /dev/null +++ b/duniter4j-es-user/src/main/misc/query_many_indices.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +echo "--- COUNT query --- " +curl -XPOST "http://127.0.0.1:9200/_search?pretty" -d' +{ + query: { + indices : { + "indices" : ["user", "registry", "currency"], + "query" : { "term" : { "tags" : "gtest" } }, + "no_match_query" : { "term" : { "tags" : "gtest" } } + } + }, + from: 0, + size: 100 +}' + + 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 50ee1d02432d6ec15541e0fd642989f29becfa23..e149f6a60fe85719287665c08940e908ef3a9a62 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 @@ -1,6 +1,7 @@ duniter.event.NODE_BMA_DOWN=Duniter node [%1$s\:%2$s] is DOWN\: no access from ES node [%3$s]. Last connexion at %4$d. Blockchain indexation waiting. duniter.event.NODE_BMA_UP=Duniter node [%1$s\:%2$s] is UP again. duniter.event.NODE_STARTED=Node started on cluster Duniter4j ES [%s] +duniter.invitation.cert.received= duniter.user.event.active= duniter.user.event.cert.received= duniter.user.event.cert.sent= 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 c41fd1df1a30d20fc71986df1e34189634261047..ab9df8238fc6befdd3f88d3a7ab63e4798bb1048 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 @@ -1,6 +1,7 @@ duniter.event.NODE_BMA_DOWN=Noeud Duniter [%1$s\:%2$s] non joignable, depuis le noeud ES API [%3$s]. Dernière connexion à %4$d. Indexation de blockchain en attente. duniter.event.NODE_BMA_UP=Noeud Duniter [%1$s\:%2$s] à nouveau accessible. duniter.event.NODE_STARTED=Noeud ES API démarré sur le cluster Duniter [%1$s] +duniter.invitation.cert.received=%2$s vous invite à certifier une identité. duniter.user.event.cert.received=%2$s vous a certifié (certification prise en compte). duniter.user.event.cert.sent=Votre certification de %2$s a été pris en compte. duniter.user.event.message.received=Vous avez reçu un message de %2$s