mirror of
https://github.com/LuckPerms/LuckPerms.git
synced 2024-11-24 03:25:19 +01:00
Add custom payload message to API (#3840)
This commit is contained in:
parent
79273a8bcc
commit
8c6586f008
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.luckperms.api.event.messaging;
|
||||
|
||||
import net.luckperms.api.event.LuckPermsEvent;
|
||||
import net.luckperms.api.event.util.Param;
|
||||
import net.luckperms.api.messaging.MessagingService;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Called when a custom payload message is received via the {@link MessagingService}.
|
||||
*
|
||||
* <p>This event is effectively the 'other end' of
|
||||
* {@link MessagingService#sendCustomMessage(String, String)}.</p>
|
||||
*
|
||||
* @since 5.5
|
||||
*/
|
||||
public interface CustomMessageReceiveEvent extends LuckPermsEvent {
|
||||
|
||||
/**
|
||||
* Gets the channel id.
|
||||
*
|
||||
* @return the channel id
|
||||
*/
|
||||
@Param(0)
|
||||
@NonNull String getChannelId();
|
||||
|
||||
/**
|
||||
* Gets the custom payload that was sent.
|
||||
*
|
||||
* @return the custom payload
|
||||
*/
|
||||
@Param(1)
|
||||
@NonNull String getPayload();
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Events relating to the {@link net.luckperms.api.messaging.MessagingService}.
|
||||
*/
|
||||
package net.luckperms.api.event.messaging;
|
@ -26,11 +26,12 @@
|
||||
package net.luckperms.api.messaging;
|
||||
|
||||
import net.luckperms.api.LuckPerms;
|
||||
import net.luckperms.api.event.messaging.CustomMessageReceiveEvent;
|
||||
import net.luckperms.api.model.user.User;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* A means to push changes to other servers using the platforms networking
|
||||
* A means to send messages to other servers using the platforms networking
|
||||
*/
|
||||
public interface MessagingService {
|
||||
|
||||
@ -71,4 +72,44 @@ public interface MessagingService {
|
||||
*/
|
||||
void pushUserUpdate(@NonNull User user);
|
||||
|
||||
/**
|
||||
* Uses the messaging service to send a message with a custom payload.
|
||||
*
|
||||
* <p>The intended use case of this functionality is to allow plugins/mods
|
||||
* to send <b>lightweight</b> and <b>permissions-related</b> custom messages
|
||||
* between instances, piggy-backing on top of the messenger abstraction
|
||||
* already built into LuckPerms.</p>
|
||||
*
|
||||
* <p>It is <b>not</b> intended as a full message broker replacement/abstraction.
|
||||
* Note that some of the messenger implementations in LuckPerms cannot handle
|
||||
* a high volume of messages being sent (for example the SQL messenger).
|
||||
* Additionally, some implementations do not give any guarantees that a message
|
||||
* will be delivered on time or even at all (for example the plugin message
|
||||
* messengers).</p>
|
||||
*
|
||||
* <p>With all of that in mind, please consider that if you are using this
|
||||
* functionality to send messages that have nothing to do with LuckPerms or
|
||||
* permissions, or that require guarantees around delivery reliability, you
|
||||
* are most likely misusing the API and would be better off building your own
|
||||
* integration with a message broker.</p>
|
||||
*
|
||||
* <p>Whilst there is (currently) no strict validation, it is recommended
|
||||
* that the channel id should use the same format as Minecraft resource locations /
|
||||
* namespaced keys. For example, a plugin called "SuperRanks" sending rank-up
|
||||
* notifications using custom payload messages might use the channel id
|
||||
* {@code "superranks:notifications"} for this purpose.</p>
|
||||
*
|
||||
* <p>The payload can be any valid UTF-8 string.</p>
|
||||
*
|
||||
* <p>The message will be delivered asynchronously.</p>
|
||||
*
|
||||
* <p>Other LuckPerms instances that receive the message will publish it to API
|
||||
* consumers using the {@link CustomMessageReceiveEvent}.</p>
|
||||
*
|
||||
* @param channelId the channel id
|
||||
* @param payload the message payload
|
||||
* @since 5.5
|
||||
*/
|
||||
void sendCustomMessage(@NonNull String channelId, @NonNull String payload);
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.luckperms.api.messenger.message.type;
|
||||
|
||||
import net.luckperms.api.messenger.message.Message;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents a "custom payload" message.
|
||||
*
|
||||
* <p>Used by API consumers to send custom messages between servers.</p>
|
||||
*
|
||||
* @see net.luckperms.api.messaging.MessagingService#sendCustomMessage(String, String)
|
||||
* @see net.luckperms.api.event.messaging.CustomMessageReceiveEvent
|
||||
* @since 5.5
|
||||
*/
|
||||
public interface CustomMessage extends Message {
|
||||
|
||||
/**
|
||||
* Gets the channel identifier.
|
||||
*
|
||||
* @return the namespace
|
||||
*/
|
||||
@NonNull String getChannelId();
|
||||
|
||||
/**
|
||||
* Gets the payload.
|
||||
*
|
||||
* @return the payload
|
||||
*/
|
||||
@NonNull String getPayload();
|
||||
|
||||
}
|
@ -54,4 +54,11 @@ public class ApiMessagingService implements MessagingService {
|
||||
Objects.requireNonNull(user, "user");
|
||||
this.handle.pushUserUpdate(ApiUser.cast(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCustomMessage(@NonNull String channelId, @NonNull String payload) {
|
||||
Objects.requireNonNull(channelId, "channelId");
|
||||
Objects.requireNonNull(payload, "payload");
|
||||
this.handle.pushCustomPayload(channelId, payload);
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ import net.luckperms.api.event.log.LogNetworkPublishEvent;
|
||||
import net.luckperms.api.event.log.LogNotifyEvent;
|
||||
import net.luckperms.api.event.log.LogPublishEvent;
|
||||
import net.luckperms.api.event.log.LogReceiveEvent;
|
||||
import net.luckperms.api.event.messaging.CustomMessageReceiveEvent;
|
||||
import net.luckperms.api.event.node.NodeAddEvent;
|
||||
import net.luckperms.api.event.node.NodeClearEvent;
|
||||
import net.luckperms.api.event.node.NodeMutateEvent;
|
||||
@ -225,6 +226,10 @@ public final class EventDispatcher {
|
||||
postAsync(LogReceiveEvent.class, id, entry);
|
||||
}
|
||||
|
||||
public void dispatchCustomMessageReceive(String channelId, String payload) {
|
||||
postAsync(CustomMessageReceiveEvent.class, channelId, payload);
|
||||
}
|
||||
|
||||
public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Difference<Node> changes) {
|
||||
if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) {
|
||||
return;
|
||||
@ -410,6 +415,7 @@ public final class EventDispatcher {
|
||||
LogNotifyEvent.class,
|
||||
LogPublishEvent.class,
|
||||
LogReceiveEvent.class,
|
||||
CustomMessageReceiveEvent.class,
|
||||
NodeAddEvent.class,
|
||||
NodeClearEvent.class,
|
||||
NodeRemoveEvent.class,
|
||||
|
@ -76,4 +76,12 @@ public interface InternalMessagingService {
|
||||
*/
|
||||
void pushLog(Action logEntry);
|
||||
|
||||
/**
|
||||
* Pushes a custom payload to connected servers.
|
||||
*
|
||||
* @param channelId the channel id
|
||||
* @param payload the payload
|
||||
*/
|
||||
void pushCustomPayload(String channelId, String payload);
|
||||
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import me.lucko.luckperms.common.actionlog.LoggedAction;
|
||||
import me.lucko.luckperms.common.cache.BufferedRequest;
|
||||
import me.lucko.luckperms.common.config.ConfigKeys;
|
||||
import me.lucko.luckperms.common.messaging.message.ActionLogMessageImpl;
|
||||
import me.lucko.luckperms.common.messaging.message.CustomMessageImpl;
|
||||
import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl;
|
||||
import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
@ -45,6 +46,7 @@ import net.luckperms.api.messenger.Messenger;
|
||||
import net.luckperms.api.messenger.MessengerProvider;
|
||||
import net.luckperms.api.messenger.message.Message;
|
||||
import net.luckperms.api.messenger.message.type.ActionLogMessage;
|
||||
import net.luckperms.api.messenger.message.type.CustomMessage;
|
||||
import net.luckperms.api.messenger.message.type.UpdateMessage;
|
||||
import net.luckperms.api.messenger.message.type.UserUpdateMessage;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
@ -69,7 +71,7 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
|
||||
this.messenger = messengerProvider.obtain(this);
|
||||
Objects.requireNonNull(this.messenger, "messenger");
|
||||
|
||||
this.receivedMessages = new ExpiringSet<>(1, TimeUnit.HOURS);
|
||||
this.receivedMessages = new ExpiringSet<>(5, TimeUnit.MINUTES);
|
||||
this.updateBuffer = new PushUpdateBuffer(plugin);
|
||||
}
|
||||
|
||||
@ -136,6 +138,14 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushCustomPayload(String channelId, String payload) {
|
||||
this.plugin.getBootstrap().getScheduler().executeAsync(() -> {
|
||||
UUID requestId = generatePingId();
|
||||
this.messenger.sendOutgoingMessage(new CustomMessageImpl(requestId, channelId, payload));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeIncomingMessage(@NonNull Message message) {
|
||||
Objects.requireNonNull(message, "message");
|
||||
@ -147,7 +157,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
|
||||
// determine if the message can be handled by us
|
||||
boolean valid = message instanceof UpdateMessage ||
|
||||
message instanceof UserUpdateMessage ||
|
||||
message instanceof ActionLogMessage;
|
||||
message instanceof ActionLogMessage ||
|
||||
message instanceof CustomMessage;
|
||||
|
||||
// instead of throwing an exception here, just return false
|
||||
// it means an instance of LP can gracefully handle messages it doesn't
|
||||
@ -209,6 +220,9 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
|
||||
case ActionLogMessageImpl.TYPE:
|
||||
decoded = ActionLogMessageImpl.decode(content, id);
|
||||
break;
|
||||
case CustomMessageImpl.TYPE:
|
||||
decoded = CustomMessageImpl.decode(content, id);
|
||||
break;
|
||||
default:
|
||||
// gracefully return if we just don't recognise the type
|
||||
return false;
|
||||
@ -270,6 +284,12 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
|
||||
|
||||
this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction());
|
||||
this.plugin.getLogDispatcher().dispatchFromRemote((LoggedAction) msg.getAction());
|
||||
|
||||
} else if (message instanceof CustomMessage) {
|
||||
CustomMessage msg = (CustomMessage) message;
|
||||
|
||||
this.plugin.getEventDispatcher().dispatchCustomMessageReceive(msg.getChannelId(), msg.getPayload());
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName());
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.messaging.message;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import me.lucko.luckperms.common.messaging.LuckPermsMessagingService;
|
||||
import me.lucko.luckperms.common.util.gson.JObject;
|
||||
import net.luckperms.api.messenger.message.type.CustomMessage;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class CustomMessageImpl extends AbstractMessage implements CustomMessage {
|
||||
public static final String TYPE = "custom";
|
||||
|
||||
public static CustomMessageImpl decode(@Nullable JsonElement content, UUID id) {
|
||||
if (content == null) {
|
||||
throw new IllegalStateException("Missing content");
|
||||
}
|
||||
|
||||
JsonObject obj = content.getAsJsonObject();
|
||||
if (!obj.has("channelId")) {
|
||||
throw new IllegalStateException("Incoming message has no 'channelId' argument: " + content);
|
||||
}
|
||||
if (!obj.has("payload")) {
|
||||
throw new IllegalStateException("Incoming message has no 'payload' argument: " + content);
|
||||
}
|
||||
|
||||
String channelId = obj.get("channelId").getAsString();
|
||||
String payload = obj.get("payload").getAsString();
|
||||
|
||||
return new CustomMessageImpl(id, channelId, payload);
|
||||
}
|
||||
|
||||
private final String channelId;
|
||||
private final String payload;
|
||||
|
||||
public CustomMessageImpl(UUID id, String channelId, String payload) {
|
||||
super(id);
|
||||
this.channelId = channelId;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getChannelId() {
|
||||
return this.channelId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getPayload() {
|
||||
return this.payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String asEncodedString() {
|
||||
return LuckPermsMessagingService.encodeMessageAsString(
|
||||
TYPE, getId(), new JObject().add("channelId", this.channelId).add("payload", this.payload).toJson()
|
||||
);
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@
|
||||
package me.lucko.luckperms.standalone;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import me.lucko.luckperms.common.actionlog.LoggedAction;
|
||||
import me.lucko.luckperms.common.messaging.InternalMessagingService;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
@ -34,9 +33,9 @@ import me.lucko.luckperms.standalone.utils.TestPluginProvider;
|
||||
import net.luckperms.api.actionlog.Action;
|
||||
import net.luckperms.api.event.EventBus;
|
||||
import net.luckperms.api.event.log.LogReceiveEvent;
|
||||
import net.luckperms.api.event.messaging.CustomMessageReceiveEvent;
|
||||
import net.luckperms.api.event.sync.PreNetworkSyncEvent;
|
||||
import net.luckperms.api.event.sync.SyncType;
|
||||
import net.luckperms.api.model.PlayerSaveResult;
|
||||
import net.luckperms.api.platform.Health;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
@ -53,9 +52,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Testcontainers
|
||||
@ -118,15 +115,24 @@ public class MessagingIntegrationTest {
|
||||
}
|
||||
});
|
||||
|
||||
CountDownLatch latch4 = new CountDownLatch(1);
|
||||
eventBus.subscribe(CustomMessageReceiveEvent.class, e -> {
|
||||
if (e.getChannelId().equals("luckperms:test") && e.getPayload().equals("hello")) {
|
||||
latch4.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// send some messages from plugin A to plugin B
|
||||
messagingServiceA.pushUpdate();
|
||||
messagingServiceA.pushUserUpdate(user);
|
||||
messagingServiceA.pushLog(exampleLogEntry);
|
||||
messagingServiceA.pushCustomPayload("luckperms:test", "hello");
|
||||
|
||||
// wait for the messages to be sent/received
|
||||
assertTrue(latch1.await(10, TimeUnit.SECONDS));
|
||||
assertTrue(latch2.await(10, TimeUnit.SECONDS));
|
||||
assertTrue(latch3.await(10, TimeUnit.SECONDS));
|
||||
assertTrue(latch4.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user