diff options
Diffstat (limited to 'src')
4 files changed, 254 insertions, 27 deletions
diff --git a/src/main/java/com/juick/formatters/PlainTextFormatter.java b/src/main/java/com/juick/formatters/PlainTextFormatter.java index 378a523f7..4cb079506 100644 --- a/src/main/java/com/juick/formatters/PlainTextFormatter.java +++ b/src/main/java/com/juick/formatters/PlainTextFormatter.java @@ -84,6 +84,14 @@ public class PlainTextFormatter { return "https://juick.com/m/" + jmsg.getMid(); } + public static String markdownUrl(String url, String description) { + if (StringUtils.isNotBlank(description)) { + return String.format("[%s](%s)", description, url); + } else { + return url; + } + } + public static String formatPostNumber(com.juick.Message jmsg) { if (jmsg.getRid() > 0) { return String.format("%d/%d", jmsg.getMid(), jmsg.getRid()); diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java index bd8b9ea85..c26941ef3 100644 --- a/src/main/java/com/juick/server/api/activity/Profile.java +++ b/src/main/java/com/juick/server/api/activity/Profile.java @@ -3,6 +3,7 @@ package com.juick.server.api.activity; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.Message; import com.juick.User; +import com.juick.formatters.PlainTextFormatter; import com.juick.model.CommandResult; import com.juick.server.ActivityPubManager; import com.juick.server.CommandsManager; @@ -259,7 +260,7 @@ public class Profile { } @PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE}) - public ResponseEntity<Void> processInbox(InputStream inboxData) throws Exception { + public ResponseEntity<CommandResult> processInbox(InputStream inboxData) throws Exception { String inbox = IOUtils.toString(inboxData, StandardCharsets.UTF_8); logger.info("Inbox: {}", inbox); Activity activity = jsonMapper.readValue(inbox, Activity.class); @@ -269,7 +270,7 @@ public class Profile { Follow followRequest = (Follow) activity; applicationEventPublisher.publishEvent( new FollowEvent(this, followRequest)); - return new ResponseEntity<>(HttpStatus.ACCEPTED); + return new ResponseEntity<>(CommandResult.fromString("Follow request accepted"), HttpStatus.ACCEPTED); } if (activity instanceof Undo) { @@ -278,10 +279,10 @@ public class Profile { String objectObject = (String) object.get("object"); if (objectType.equals("Follow")) { applicationEventPublisher.publishEvent(new UndoFollowEvent(this, activity.getActor(), objectObject)); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(CommandResult.fromString("Undo follow request accepted"), HttpStatus.OK); } else if (objectType.equals("Like") || objectType.equals("Announce")) { applicationEventPublisher.publishEvent(new UndoAnnounceEvent(this, activity.getActor(), objectObject)); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(CommandResult.fromString("Undo like/announce request accepted"), HttpStatus.OK); } } if (activity instanceof Create) { @@ -290,7 +291,7 @@ public class Profile { if (note.get("type").equals("Note")) { URI noteId = URI.create((String) note.get("id")); if (messagesService.replyExists(noteId)) { - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(CommandResult.fromString("Reply already exists"), HttpStatus.OK); } else { String inReplyTo = (String) note.get("inReplyTo"); if (StringUtils.isNotBlank(inReplyTo)) { @@ -298,38 +299,50 @@ public class Profile { String postId = activityPubManager.postId(inReplyTo); User user = new User(); user.setUri(URI.create(activity.getActor())); - String attachment = StringUtils.EMPTY; - if (note.get("attachment") != null && ((List) note.get("attachment")).size() > 0) { - Map<String, Object> attachmentObj = (Map<String, Object>) ((List<Object>) note.get("attachment")).get(0); - attachment = (String) attachmentObj.get("url"); - } - String markdown = remarkConverter.convertFragment((String)note.get("content")); - CommandResult result = commandsManager.processCommand(user, String.format("#%s %s", postId, markdown), URI.create(attachment)); + String markdown = remarkConverter.convertFragment((String) note.get("content")); + String commandBody = note.get("attachment") == null ? markdown : + ((List<Object>) note.get("attachment")).stream().map(attachmentObj -> { + Map<String, String> attachment = (Map<String, String>) attachmentObj; + String attachmentUrl = attachment.get("url"); + String attachmentName = attachment.get("name"); + return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); + }).reduce((source, url) -> String.format("%s\n%s", source, url)) + .orElse(markdown); + + CommandResult result = commandsManager.processCommand( + user, String.format("#%s %s", postId, commandBody), + URI.create(StringUtils.EMPTY)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { messagesService.updateReplyUri(result.getNewMessage().get(), noteId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(result, HttpStatus.OK); } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); } } else { Message reply = messagesService.getReplyByUri(inReplyTo); if (reply != null) { User user = new User(); user.setUri(URI.create(activity.getActor())); - String attachment = StringUtils.EMPTY; - if (note.get("attachment") != null && ((List) note.get("attachment")).size() > 0) { - Map<String, Object> attachmentObj = (Map<String, Object>) ((List<Object>) note.get("attachment")).get(0); - attachment = (String) attachmentObj.get("url"); - } String markdown = remarkConverter.convertFragment((String)note.get("content")); - CommandResult result = commandsManager.processCommand(user, String.format("#%d/%d %s", reply.getMid(), reply.getRid(), markdown), URI.create(attachment)); + String commandBody = note.get("attachment") == null ? markdown : + ((List<Object>) note.get("attachment")).stream().map(attachmentObj -> { + Map<String, String> attachment = (Map<String, String>) attachmentObj; + String attachmentUrl = attachment.get("url"); + String attachmentName = attachment.get("name"); + return PlainTextFormatter.markdownUrl(attachmentUrl, attachmentName); + }).reduce((source, url) -> String.format("%s\n%s", source, url)) + .orElse(markdown); + CommandResult result = commandsManager.processCommand( + user, + String.format("#%d/%d %s", reply.getMid(), reply.getRid(), commandBody), + URI.create(StringUtils.EMPTY)); logger.info(jsonMapper.writeValueAsString(result)); if (result.getNewMessage().isPresent()) { messagesService.updateReplyUri(result.getNewMessage().get(), noteId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(result, HttpStatus.OK); } else { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); } } } @@ -344,25 +357,25 @@ public class Profile { URI actor = URI.create(activity.getActor()); URI reply = URI.create((String)tombstone.get("id")); messagesService.deleteReply(actor, reply); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.OK); } } if (activity instanceof Like || activity instanceof Announce) { applicationEventPublisher.publishEvent(new AnnounceEvent(this, activity.getActor(), (String)activity.getObject())); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(CommandResult.fromString("Like/announce request accepted"), HttpStatus.OK); } logger.warn("Unknown activity: {}", jsonMapper.writeValueAsString(activity)); - return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + return new ResponseEntity<>(CommandResult.fromString("Unknown activity"), HttpStatus.NOT_IMPLEMENTED); } if (activity instanceof Delete) { if (activity.getObject() instanceof String) { // Delete gone user if (activity.getActor().equals(activity.getObject())) { - return new ResponseEntity<>(HttpStatus.ACCEPTED); + return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"), HttpStatus.ACCEPTED); } } } - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + return new ResponseEntity<>(CommandResult.fromString("Can not authenticate"), HttpStatus.UNAUTHORIZED); } @PostMapping(value = "/u/", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public User fetchUser(@RequestParam URI uri) { diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 9e1e68ea3..b1a71c65e 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -44,6 +44,7 @@ import com.juick.server.util.ImageUtils; import com.juick.server.www.WebApp; import com.juick.service.*; import com.juick.service.component.MessageEvent; +import com.juick.test.util.MockUtils; import com.juick.util.DateFormattersHolder; import com.juick.util.MessageUtils; import com.mitchellbosecke.pebble.PebbleEngine; @@ -209,6 +210,8 @@ public class ServerTests { private Resource cmykJpeg; @Value("classpath:nojfif.jpg") private Resource nojfif; + @Value("classpath:hubzilla_activity.json") + private Resource hubzillaActivity; @Inject private KeystoreManager testKeystoreManager; @@ -1932,4 +1935,22 @@ public class ServerTests { .andExpect(status().isAccepted()); apClient.setRequestFactory(originalRequestFactory); } + + @Test + public void legacyAvatarEndpoint() throws Exception { + mockMvc.perform(get("/api/avatar") + .param("uname", "unknown")) + .andExpect(status().isOk()) + .andExpect(content().bytes(IOUtils.toByteArray(defaultAvatar.getInputStream()))); + } + @Test + public void federatedAttachmentsAsLinks() throws Exception { + int mid = messagesService.createMessage(ugnich.getUid(), "test", StringUtils.EMPTY, Collections.emptyList()); + Message testMessage = MockUtils.mockMessage(mid, freefd, "reply"); + } + @Test + public void hubzillaActor() throws Exception { + String activity = IOUtils.toString(hubzillaActivity.getInputStream(), StandardCharsets.UTF_8); + Create create = jsonMapper.readValue(activity, Create.class); + } } diff --git a/src/test/resources/hubzilla_activity.json b/src/test/resources/hubzilla_activity.json new file mode 100644 index 000000000..b25aca87d --- /dev/null +++ b/src/test/resources/hubzilla_activity.json @@ -0,0 +1,185 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://ussr.win/apschema/v1.2" + ], + "type": "Create", + "id": "https://ussr.win/activity/e46a6100e4ba83d602eab5fe0e035405a32d7d36094c95dc85c5c9fcd3d93ab2%40ussr.win", + "published": "2019-02-14T11:23:29Z", + "updated": "2019-02-14T11:38:48Z", + "actor": { + "type": "Person", + "id": "https://ussr.win/channel/zlax", + "preferredUsername": "zlax", + "name": "ivan zlax", + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://ussr.win/photo/profile/l/2", + "height": 300, + "width": 300 + }, + "url": { + "type": "Link", + "mediaType": "text/html", + "href": "https://ussr.win/channel/zlax" + }, + "inbox": "https://ussr.win/inbox/zlax", + "outbox": "https://ussr.win/outbox/zlax", + "followers": "https://ussr.win/followers/zlax", + "following": "https://ussr.win/following/zlax", + "endpoints": { + "sharedInbox": "https://ussr.win/inbox" + }, + "publicKey": { + "id": "https://ussr.win/channel/zlax/public_key_pem", + "owner": "https://ussr.win/channel/zlax", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtFSIKFVzo+9NzzY0xho9\nA8CqO0j12f3HEOZUDBDj1NBbQ6Cj1p1f5mB1AlRr+a05fqpraCWIJyCXVTtdyjem\niQ/ObT2PuZIhAY04/ptv78S9EM9ImZi2aDNWp2nWQu23dJajb2TMpIxGNS1M3DVR\ngXwRVEVIEEDzftWx7vF4335/2uJvPfwTzKI3pR972xp1iZCd/G3BtYzUW4swIFSu\n+5ZLKhgwO6A4Ge5+sn2k6x+K56YCgeEEQheOg+PbggIQ68xfvYCQyfuShqAXEadv\nOvQ1dN8WO+tgKqYDRQgILIONB8/2/XXMoQbSVRZs2GmLKXHhIeVKiSZ1vKwHsPx1\nCYUayQo/BB1JpbjkmyDQcHAX+KZ7PQUhpQn976f6Ycp5rznvBH5Zm96VKySaGMy7\nGWefafOfcCNfVZpq17gPPfSbC0XOHdvm6T1e1OPwFl/ho9HxatbA60DIDEd7xf4V\nq9aTSZ2MeQW5JXaxJXSgwucPZ9mhVfucFtCyLdlHZ7gA5yJ+pyYmMe8pjCSNWztD\nUOoJC46vv2l1RbMGTJy2AY9ZuPvyrmJHlsRsTYcmDYRFSk6sgGi1eswSDHlruE3u\nKG4E0nsA8qxzgzpCViQV4DIgXhMInBwo7ZhSyWD+tF2a4tpXorHqmg35x/aqJlaq\nE2xF+yrfh/Kx8O+7P6C1aGcCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "nomadicLocations": [ + { + "id": "https://ussr.win/locs/zlax", + "type": "nomadicLocation", + "locationAddress": "acct:zlax@ussr.win", + "locationPrimary": true, + "locationDeleted": false + } + ] + }, + "object": { + "type": "Note", + "id": "https://ussr.win/item/e46a6100e4ba83d602eab5fe0e035405a32d7d36094c95dc85c5c9fcd3d93ab2%40ussr.win", + "published": "2019-02-14T11:23:29Z", + "updated": "2019-02-14T11:38:48Z", + "url": { + "type": "text/html", + "rel": "alternate", + "href": "https://ussr.win/channel/zlax/?f=&mid=e46a6100e4ba83d602eab5fe0e035405a32d7d36094c95dc85c5c9fcd3d93ab2@ussr.win" + }, + "attributedTo": "https://ussr.win/channel/zlax", + "content": "<span class=\"bookmark-identifier\">#^</span><a class=\"bookmark\" href=\"https://twitter.com/Dalatrm/status/1095677403198906369\" target=\"_blank\" rel=\"nofollow noopener\" >https://twitter.com/Dalatrm/status/1095677403198906369</a><br /><span class=\"bookmark-identifier\">#^</span><a class=\"bookmark\" href=\"https://twitter.com/Dalatrm/status/1095677401433022466\" target=\"_blank\" rel=\"nofollow noopener\" >https://twitter.com/Dalatrm/status/1095677401433022466</a><br /><img class=\"zrl\" src=\"https://ussr.win/photo/6776e315a9692860af53c19518e524f50ab047a4317435403532c922a7c484d4-2.jpg\" style=\"width: 100%; max-width: 590px;\" alt=\"Image/photo\" /><br /><br /><img class=\"zrl\" src=\"https://ussr.win/photo/afb21af40be39ad19589dec5dfa0161b2364b59331bba95aae823e68b3acfbf3-2.jpg\" style=\"width: 100%; max-width: 623px;\" alt=\"Image/photo\" /><br />Продюсер BBC признал постановочными сцены после химатаки в Сирии<br /><span class=\"bookmark-identifier\">#^</span><a class=\"bookmark\" href=\"https://www.vedomosti.ru/politics/news/2019/02/14/794104-bbc-himataki\" target=\"_blank\" rel=\"nofollow noopener\" >https://www.vedomosti.ru/politics/news/2019/02/14/794104-bbc-himataki</a><br /><blockquote>Сцены в госпитале сирийского города Дума после предполагаемой химической атаки были постановочными для достижения «максимального эффекта». Об этом в своем твиттере написал продюсер BBC по Сирии Риам Далати. «Спустя почти шесть месяцев расследования я могу подтвердить без тени сомнения, что сцены в госпитале Думы были постановочные. Погибших в больнице не было», — признал он.<br /><br />Сам факт химической атаки, якобы имевшей место 7 апреля 2018 г., пока не подтвержден Организацией по запрещению химического оружия (ОЗХО). «Атака была, зарин не использовался, но мы должны дождаться, когда ОЗХО подтвердит, использовался ли хлор или что-то еще. Однако все остальное вокруг атаки было создано для максимального эффекта», — написал Далати.<br /><br />Информацию о применении в сирийской Думе химического оружия с использованием хлора и нервно-паралитического агента распространили общественные организации, в том числе «Белые каски». По данным организации, жертвами атаки стали около 70 человек.</blockquote><br />#<a class=\"zrl\" href=\"https://ussr.win/search?tag=bbc\" target=\"_blank\" rel=\"nofollow noopener\" >bbc</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=capitalism\" target=\"_blank\" rel=\"nofollow noopener\" >capitalism</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=conspiracy\" target=\"_blank\" rel=\"nofollow noopener\" >conspiracy</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=history\" target=\"_blank\" rel=\"nofollow noopener\" >history</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=hoax\" target=\"_blank\" rel=\"nofollow noopener\" >hoax</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=metaprogramming\" target=\"_blank\" rel=\"nofollow noopener\" >metaprogramming</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=revision\" target=\"_blank\" rel=\"nofollow noopener\" >revision</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=syria\" target=\"_blank\" rel=\"nofollow noopener\" >syria</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=terrorism\" target=\"_blank\" rel=\"nofollow noopener\" >terrorism</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=uk\" target=\"_blank\" rel=\"nofollow noopener\" >uk</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=war\" target=\"_blank\" rel=\"nofollow noopener\" >war</a> #<a class=\"zrl\" href=\"https://ussr.win/search?tag=whitehelments\" target=\"_blank\" rel=\"nofollow noopener\" >whitehelments</a>", + "actor": { + "type": "Person", + "id": "https://ussr.win/channel/zlax", + "preferredUsername": "zlax", + "name": "ivan zlax", + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://ussr.win/photo/profile/l/2", + "height": 300, + "width": 300 + }, + "url": { + "type": "Link", + "mediaType": "text/html", + "href": "https://ussr.win/channel/zlax" + }, + "inbox": "https://ussr.win/inbox/zlax", + "outbox": "https://ussr.win/outbox/zlax", + "followers": "https://ussr.win/followers/zlax", + "following": "https://ussr.win/following/zlax", + "endpoints": { + "sharedInbox": "https://ussr.win/inbox" + }, + "publicKey": { + "id": "https://ussr.win/channel/zlax/public_key_pem", + "owner": "https://ussr.win/channel/zlax", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtFSIKFVzo+9NzzY0xho9\nA8CqO0j12f3HEOZUDBDj1NBbQ6Cj1p1f5mB1AlRr+a05fqpraCWIJyCXVTtdyjem\niQ/ObT2PuZIhAY04/ptv78S9EM9ImZi2aDNWp2nWQu23dJajb2TMpIxGNS1M3DVR\ngXwRVEVIEEDzftWx7vF4335/2uJvPfwTzKI3pR972xp1iZCd/G3BtYzUW4swIFSu\n+5ZLKhgwO6A4Ge5+sn2k6x+K56YCgeEEQheOg+PbggIQ68xfvYCQyfuShqAXEadv\nOvQ1dN8WO+tgKqYDRQgILIONB8/2/XXMoQbSVRZs2GmLKXHhIeVKiSZ1vKwHsPx1\nCYUayQo/BB1JpbjkmyDQcHAX+KZ7PQUhpQn976f6Ycp5rznvBH5Zm96VKySaGMy7\nGWefafOfcCNfVZpq17gPPfSbC0XOHdvm6T1e1OPwFl/ho9HxatbA60DIDEd7xf4V\nq9aTSZ2MeQW5JXaxJXSgwucPZ9mhVfucFtCyLdlHZ7gA5yJ+pyYmMe8pjCSNWztD\nUOoJC46vv2l1RbMGTJy2AY9ZuPvyrmJHlsRsTYcmDYRFSk6sgGi1eswSDHlruE3u\nKG4E0nsA8qxzgzpCViQV4DIgXhMInBwo7ZhSyWD+tF2a4tpXorHqmg35x/aqJlaq\nE2xF+yrfh/Kx8O+7P6C1aGcCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "nomadicLocations": [ + { + "id": "https://ussr.win/locs/zlax", + "type": "nomadicLocation", + "locationAddress": "acct:zlax@ussr.win", + "locationPrimary": true, + "locationDeleted": false + } + ] + }, + "tag": [ + { + "id": "https://ussr.win/search?tag=metaprogramming", + "name": "#metaprogramming" + }, + { + "id": "https://ussr.win/search?tag=whitehelments", + "name": "#whitehelments" + }, + { + "id": "https://ussr.win/search?tag=capitalism", + "name": "#capitalism" + }, + { + "id": "https://ussr.win/search?tag=conspiracy", + "name": "#conspiracy" + }, + { + "id": "https://ussr.win/search?tag=terrorism", + "name": "#terrorism" + }, + { + "id": "https://ussr.win/search?tag=revision", + "name": "#revision" + }, + { + "id": "https://ussr.win/search?tag=history", + "name": "#history" + }, + { + "id": "https://ussr.win/search?tag=syria", + "name": "#syria" + }, + { + "id": "https://ussr.win/search?tag=hoax", + "name": "#hoax" + }, + { + "id": "https://ussr.win/search?tag=bbc", + "name": "#bbc" + }, + { + "id": "https://ussr.win/search?tag=war", + "name": "#war" + }, + { + "id": "https://ussr.win/search?tag=uk", + "name": "#uk" + } + ], + "attachment": [ + { + "type": "Image", + "url": "https://ussr.win/photo/6776e315a9692860af53c19518e524f50ab047a4317435403532c922a7c484d4-2.jpg" + }, + { + "type": "Image", + "url": "https://ussr.win/photo/afb21af40be39ad19589dec5dfa0161b2364b59331bba95aae823e68b3acfbf3-2.jpg" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://ussr.win/followers/zlax" + ] + }, + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://ussr.win/followers/zlax" + ], + "signature": { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + "type": "RsaSignature2017", + "nonce": "c4f8b31f4277d4322a528697d550aefd78a4a7709f0da25285032f7529d3f550", + "creator": "https://ussr.win/channel/zlax/public_key_pem", + "created": "2019-02-14T11:39:01Z", + "signatureValue": "WnXFcSm3nR1Su4QwSVJq6vQ7xdtPQTfs5iTMcAcxasoGHphJaAZRISV1QRPuMhw5jap4Lmb4ZaFRfqGm4M4HND+mI06dC2HlgCh0vnNvGsbcDa2RybSnIUo/xswEa/Mcpf6uy2dcLwaZY6tXOkkJmtZOseN54CUO3bQHbd3KmSiqkvREMXv2+qrVJhohZ4R9tLXamUTkoJmq1wSS2s8XSUlmO2v+Th3OAEoHeiS1SehglvIzua83IKlU2/iAs8akGVyYng5uLO7YeUsmiKHggef1ss8XKcNuhaAW8b3DUTnUwBZCPiiCoeXEQMfYdciCtTAKARHLr5RlHPE7etf1Fqk9Pozo/b2EDpfTxbZ26ZKY02pl4g82u95L43hTmCLbZ+wbKt5M0uHRld+/WkJrkIVS2Vj0MJ84Pp94Ij5A0MaXnBDiIj8YpvbRWWVEdXWKs/N8Dy0c11NiHCPhurZTMQDwv96bBphLQGlTp4USYjbKlS95JyyxbfjRbbubM4Q3MGsZFWbRi7C0exvZiw8zHBGn3hnbnZsDdlDax2weuXsSzHPlw8DxG6XEXZy3GWuK4LlQCVXaa9AcSd9LODVIrxaelr0pdW38a2rot1rlkGHdYPR8oD5zV3fj4kWs3n68mU9VJRTvubbX1zKV8EIX9UvVJ6wuRHqyVOFKJPkKloc=" + } +}
\ No newline at end of file |