Added TurnContextTests
This commit is contained in:
Родитель
7ae65c6a21
Коммит
b774350074
|
@ -8,6 +8,7 @@ import com.microsoft.bot.schema.ConversationReference;
|
|||
import com.microsoft.bot.schema.ConversationReferenceHelper;
|
||||
import com.microsoft.bot.schema.ResourceResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Function;
|
||||
|
@ -81,7 +82,7 @@ public abstract class BotAdapter {
|
|||
* the receiving channel assigned to the activities.
|
||||
* {@link TurnContext#onSendActivities(SendActivitiesHandler)}
|
||||
*/
|
||||
public abstract CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, Activity[] activities);
|
||||
public abstract CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, List<Activity> activities);
|
||||
|
||||
/**
|
||||
* When overridden in a derived class, replaces an existing activity in the
|
||||
|
|
|
@ -279,7 +279,7 @@ public class BotFrameworkAdapter extends BotAdapter {
|
|||
*/
|
||||
@SuppressWarnings("checkstyle:EmptyBlock")
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, Activity[] activities) {
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, List<Activity> activities) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context");
|
||||
}
|
||||
|
@ -288,20 +288,20 @@ public class BotFrameworkAdapter extends BotAdapter {
|
|||
throw new IllegalArgumentException("activities");
|
||||
}
|
||||
|
||||
if (activities.length == 0) {
|
||||
if (activities.size() == 0) {
|
||||
throw new IllegalArgumentException("Expecting one or more activities, but the array was empty.");
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
ResourceResponse[] responses = new ResourceResponse[activities.length];
|
||||
ResourceResponse[] responses = new ResourceResponse[activities.size()];
|
||||
|
||||
/*
|
||||
* NOTE: we're using for here (vs. foreach) because we want to simultaneously index into the
|
||||
* activities array to get the activity to process as well as use that index to assign
|
||||
* the response to the responses array and this is the most cost effective way to do that.
|
||||
*/
|
||||
for (int index = 0; index < activities.length; index++) {
|
||||
Activity activity = activities[index];
|
||||
for (int index = 0; index < activities.size(); index++) {
|
||||
Activity activity = activities.get(index);
|
||||
ResourceResponse response = null;
|
||||
|
||||
if (activity.isType(ActivityTypes.DELAY)) {
|
||||
|
|
|
@ -5,8 +5,10 @@ package com.microsoft.bot.builder;
|
|||
|
||||
import com.microsoft.bot.schema.Activity;
|
||||
import com.microsoft.bot.schema.ConversationReference;
|
||||
import com.microsoft.bot.schema.InputHints;
|
||||
import com.microsoft.bot.schema.ResourceResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
|
@ -50,7 +52,7 @@ public class DelegatingTurnContext implements TurnContext {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak, String inputHint) {
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak, InputHints inputHint) {
|
||||
return innerTurnContext.sendActivity(textReplyToSend, speak, inputHint);
|
||||
}
|
||||
|
||||
|
@ -60,7 +62,7 @@ public class DelegatingTurnContext implements TurnContext {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(Activity[] activities) {
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(List<Activity> activities) {
|
||||
return innerTurnContext.sendActivities(activities);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,10 @@ package com.microsoft.bot.builder;
|
|||
|
||||
import com.microsoft.bot.schema.Activity;
|
||||
import com.microsoft.bot.schema.ConversationReference;
|
||||
import com.microsoft.bot.schema.InputHints;
|
||||
import com.microsoft.bot.schema.ResourceResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
|
@ -117,7 +119,7 @@ public interface TurnContext {
|
|||
* rate, volume, pronunciation, and pitch, specify {@code speak} in
|
||||
* Speech Synthesis Markup Language (SSML) format.</p>
|
||||
*/
|
||||
CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak, String inputHint);
|
||||
CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak, InputHints inputHint);
|
||||
|
||||
/**
|
||||
* Sends an activity to the sender of the incoming activity.
|
||||
|
@ -139,12 +141,12 @@ public interface TurnContext {
|
|||
* an array of {@link ResourceResponse} objects containing the IDs that
|
||||
* the receiving channel assigned to the activities.
|
||||
*/
|
||||
CompletableFuture<ResourceResponse[]> sendActivities(Activity[] activities);
|
||||
CompletableFuture<ResourceResponse[]> sendActivities(List<Activity> activities);
|
||||
|
||||
/**
|
||||
* Replaces an existing activity.
|
||||
*
|
||||
* @param activity New replacement activity.
|
||||
* @param withActivity New replacement activity.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* If the activity is successfully sent, the task result contains
|
||||
* a {@link ResourceResponse} object containing the ID that the receiving
|
||||
|
@ -152,7 +154,7 @@ public interface TurnContext {
|
|||
* <p>Before calling this, set the ID of the replacement activity to the ID
|
||||
* of the activity to replace.</p>
|
||||
*/
|
||||
CompletableFuture<ResourceResponse> updateActivity(Activity activity);
|
||||
CompletableFuture<ResourceResponse> updateActivity(Activity withActivity);
|
||||
|
||||
/**
|
||||
* Deletes an existing activity.
|
||||
|
@ -178,7 +180,7 @@ public interface TurnContext {
|
|||
* @param handler The handler to add to the context object.
|
||||
* @return The updated context object.
|
||||
* When the context's {@link #sendActivity(Activity)}
|
||||
* or {@link #sendActivities(Activity[])} methods are called,
|
||||
* or {@link #sendActivities(List<Activity>)} methods are called,
|
||||
* the adapter calls the registered handlers in the order in which they were
|
||||
* added to the context object.
|
||||
*/
|
||||
|
|
|
@ -1,133 +1,80 @@
|
|||
package com.microsoft.bot.builder;
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.microsoft.bot.schema.Activity;
|
||||
import com.microsoft.bot.schema.ActivityTypes;
|
||||
import com.microsoft.bot.schema.ConversationReference;
|
||||
import com.microsoft.bot.schema.InputHints;
|
||||
import com.microsoft.bot.schema.ResourceResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static com.microsoft.bot.schema.ActivityTypes.MESSAGE;
|
||||
import static com.microsoft.bot.schema.ActivityTypes.TRACE;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Provides context for a turn of a bot.
|
||||
* Context provides information needed to process an incoming activity.
|
||||
* The context object is created by a {@link BotAdapter} and persists for the
|
||||
* length of the turn.
|
||||
* {@linkalso Bot}
|
||||
* {@linkalso Middleware}
|
||||
* {@link Bot}
|
||||
* {@link Middleware}
|
||||
*/
|
||||
public class TurnContextImpl implements TurnContext, AutoCloseable {
|
||||
/**
|
||||
* The bot adapter that created this context object.
|
||||
*/
|
||||
private final BotAdapter adapter;
|
||||
|
||||
/**
|
||||
* The activity associated with this turn; or null when processing a proactive message.
|
||||
*/
|
||||
private final Activity activity;
|
||||
|
||||
private final List<SendActivitiesHandler> onSendActivities = new ArrayList<SendActivitiesHandler>();
|
||||
private final List<UpdateActivityHandler> onUpdateActivity = new ArrayList<UpdateActivityHandler>();
|
||||
private final List<DeleteActivityHandler> onDeleteActivity = new ArrayList<DeleteActivityHandler>();
|
||||
|
||||
/**
|
||||
* The services registered on this context object.
|
||||
*/
|
||||
private final TurnContextStateCollection turnState;
|
||||
|
||||
/**
|
||||
* Indicates whether at least one response was sent for the current turn.
|
||||
*/
|
||||
private Boolean responded = false;
|
||||
|
||||
/**
|
||||
* Creates a context object.
|
||||
*
|
||||
* @param adapter The adapter creating the context.
|
||||
* @param activity The incoming activity for the turn;
|
||||
* @param withAdapter The adapter creating the context.
|
||||
* @param withActivity The incoming activity for the turn;
|
||||
* or {@code null} for a turn for a proactive message.
|
||||
* @throws IllegalArgumentException {@code activity} or
|
||||
* {@code adapter} is {@code null}.
|
||||
* For use by bot adapter implementations only.
|
||||
*/
|
||||
public TurnContextImpl(BotAdapter adapter, Activity activity) {
|
||||
if (adapter == null)
|
||||
public TurnContextImpl(BotAdapter withAdapter, Activity withActivity) {
|
||||
if (withAdapter == null) {
|
||||
throw new IllegalArgumentException("adapter");
|
||||
this.adapter = adapter;
|
||||
if (activity == null)
|
||||
}
|
||||
adapter = withAdapter;
|
||||
|
||||
if (withActivity == null) {
|
||||
throw new IllegalArgumentException("activity");
|
||||
this.activity = activity;
|
||||
}
|
||||
activity = withActivity;
|
||||
|
||||
turnState = new TurnContextStateCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a conversation reference from an activity.
|
||||
*
|
||||
* @param activity The activity.
|
||||
* @return A conversation reference for the conversation that contains the activity.
|
||||
* @throws IllegalArgumentException {@code activity} is {@code null}.
|
||||
*/
|
||||
public static ConversationReference getConversationReference(Activity activity) {
|
||||
BotAssert.activityNotNull(activity);
|
||||
|
||||
ConversationReference r = new ConversationReference() {{
|
||||
setActivityId(activity.getId());
|
||||
setUser(activity.getFrom());
|
||||
setBot(activity.getRecipient());
|
||||
setConversation(activity.getConversation());
|
||||
setChannelId(activity.getChannelId());
|
||||
setServiceUrl(activity.getServiceUrl());
|
||||
}};
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an activity with the delivery information from an existing
|
||||
* conversation reference.
|
||||
*
|
||||
* @param activity The activity to update.
|
||||
* @param reference The conversation reference.
|
||||
*/
|
||||
public static Activity applyConversationReference(Activity activity, ConversationReference reference) {
|
||||
return applyConversationReference(activity, reference, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an activity with the delivery information from an existing
|
||||
* conversation reference.
|
||||
*
|
||||
* @param activity The activity to update.
|
||||
* @param reference The conversation reference.
|
||||
* @param isIncoming (Optional) {@code true} to treat the activity as an
|
||||
* incoming activity, where the bot is the recipient; otherwaire {@code false}.
|
||||
* Default is {@code false}, and the activity will show the bot as the sender.
|
||||
* Call {@link #getConversationReference(Activity)} on an incoming
|
||||
* activity to get a conversation reference that you can then use to update an
|
||||
* outgoing activity with the correct delivery information.
|
||||
* <p>The {@link #sendActivity(Activity)} and {@link #sendActivities(Activity[])}
|
||||
* methods do this for you.</p>
|
||||
*/
|
||||
public static Activity applyConversationReference(Activity activity,
|
||||
ConversationReference reference,
|
||||
boolean isIncoming) {
|
||||
|
||||
activity.setChannelId(reference.getChannelId());
|
||||
activity.setServiceUrl(reference.getServiceUrl());
|
||||
activity.setConversation(reference.getConversation());
|
||||
|
||||
if (isIncoming) {
|
||||
activity.setFrom(reference.getUser());
|
||||
activity.setRecipient(reference.getBot());
|
||||
if (reference.getActivityId() != null)
|
||||
activity.setId(reference.getActivityId());
|
||||
} else { // Outgoing
|
||||
activity.setFrom(reference.getBot());
|
||||
activity.setRecipient(reference.getUser());
|
||||
if (reference.getActivityId() != null)
|
||||
activity.setReplyToId(reference.getActivityId());
|
||||
}
|
||||
return activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a response handler for send activity operations.
|
||||
*
|
||||
|
@ -135,16 +82,17 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
* @return The updated context object.
|
||||
* @throws IllegalArgumentException {@code handler} is {@code null}.
|
||||
* When the context's {@link #sendActivity(Activity)}
|
||||
* or {@link #sendActivities(Activity[])} methods are called,
|
||||
* or {@link #sendActivities(List<Activity>)} methods are called,
|
||||
* the adapter calls the registered handlers in the order in which they were
|
||||
* added to the context object.
|
||||
*/
|
||||
@Override
|
||||
public TurnContext onSendActivities(SendActivitiesHandler handler) {
|
||||
if (handler == null)
|
||||
if (handler == null) {
|
||||
throw new IllegalArgumentException("handler");
|
||||
}
|
||||
|
||||
this.onSendActivities.add(handler);
|
||||
onSendActivities.add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -160,10 +108,11 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
*/
|
||||
@Override
|
||||
public TurnContext onUpdateActivity(UpdateActivityHandler handler) {
|
||||
if (handler == null)
|
||||
if (handler == null) {
|
||||
throw new IllegalArgumentException("handler");
|
||||
}
|
||||
|
||||
this.onUpdateActivity.add(handler);
|
||||
onUpdateActivity.add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -179,10 +128,11 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
*/
|
||||
@Override
|
||||
public TurnContext onDeleteActivity(DeleteActivityHandler handler) {
|
||||
if (handler == null)
|
||||
if (handler == null) {
|
||||
throw new IllegalArgumentException("handler");
|
||||
}
|
||||
|
||||
this.onDeleteActivity.add(handler);
|
||||
onDeleteActivity.add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -213,58 +163,90 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
* Indicates whether at least one response was sent for the current turn.
|
||||
*
|
||||
* @return {@code true} if at least one response was sent for the current turn.
|
||||
* @throws IllegalArgumentException You attempted to set the value to {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean getResponded() {
|
||||
return this.responded;
|
||||
}
|
||||
|
||||
private void setResponded(boolean responded) {
|
||||
if (responded == false) {
|
||||
throw new IllegalArgumentException("TurnContext: cannot set 'responded' to a value of 'false'.");
|
||||
}
|
||||
this.responded = true;
|
||||
return responded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message activity to the sender of the incoming activity.
|
||||
*
|
||||
* <p>If the activity is successfully sent, the task result contains
|
||||
* a {@link ResourceResponse} object containing the ID that the receiving
|
||||
* channel assigned to the activity.</p>
|
||||
*
|
||||
* <p>See the channel's documentation for limits imposed upon the contents of
|
||||
* {@code textReplyToSend}.</p>
|
||||
*
|
||||
* @param textReplyToSend The text of the message to send.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace.
|
||||
* If the activity is successfully sent, the task result contains
|
||||
* a {@link ResourceResponse} object containing the ID that the receiving
|
||||
* channel assigned to the activity.
|
||||
* <p>See the channel's documentation for limits imposed upon the contents of
|
||||
* {@code textReplyToSend}.</p>
|
||||
* <p>To control various characteristics of your bot's speech such as voice,
|
||||
* rate, volume, pronunciation, and pitch, specify {@code speak} in
|
||||
* Speech Synthesis Markup Language (SSML) format.</p>
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend) {
|
||||
return sendActivity(textReplyToSend, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message activity to the sender of the incoming activity.
|
||||
*
|
||||
* <p>If the activity is successfully sent, the task result contains
|
||||
* a {@link ResourceResponse} object containing the ID that the receiving
|
||||
* channel assigned to the activity.</p>
|
||||
*
|
||||
* <p>See the channel's documentation for limits imposed upon the contents of
|
||||
* {@code textReplyToSend}.</p>
|
||||
*
|
||||
* @param textReplyToSend The text of the message to send.
|
||||
* @param speak To control various characteristics of your bot's speech such as voice
|
||||
* rate, volume, pronunciation, and pitch, specify Speech Synthesis Markup
|
||||
* Language (SSML) format.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak) {
|
||||
return sendActivity(textReplyToSend, speak, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message activity to the sender of the incoming activity.
|
||||
*
|
||||
* <p>If the activity is successfully sent, the task result contains
|
||||
* a {@link ResourceResponse} object containing the ID that the receiving
|
||||
* channel assigned to the activity.</p>
|
||||
*
|
||||
* <p>See the channel's documentation for limits imposed upon the contents of
|
||||
* {@code textReplyToSend}.</p>
|
||||
*
|
||||
* @param textReplyToSend The text of the message to send.
|
||||
* @param speak To control various characteristics of your bot's speech such as voice
|
||||
* rate, volume, pronunciation, and pitch, specify Speech Synthesis Markup
|
||||
* Language (SSML) format.
|
||||
* @param inputHint (Optional) Input hint.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws IllegalArgumentException {@code textReplyToSend} is {@code null} or whitespace.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend, String speak, String inputHint) {
|
||||
if (StringUtils.isEmpty(textReplyToSend))
|
||||
public CompletableFuture<ResourceResponse> sendActivity(String textReplyToSend,
|
||||
String speak,
|
||||
InputHints inputHint) {
|
||||
if (StringUtils.isEmpty(textReplyToSend)) {
|
||||
throw new IllegalArgumentException("textReplyToSend");
|
||||
}
|
||||
|
||||
Activity activityToSend = new Activity(MESSAGE) {{
|
||||
Activity activityToSend = new Activity(ActivityTypes.MESSAGE) {{
|
||||
setText(textReplyToSend);
|
||||
}};
|
||||
if (speak != null)
|
||||
activityToSend.setSpeak(speak);
|
||||
|
||||
if (StringUtils.isNotEmpty(inputHint))
|
||||
activityToSend.setInputHint(InputHints.fromString(inputHint));
|
||||
if (StringUtils.isNotEmpty(speak)) {
|
||||
activityToSend.setSpeak(speak);
|
||||
}
|
||||
|
||||
if (inputHint != null) {
|
||||
activityToSend.setInputHint(inputHint);
|
||||
}
|
||||
|
||||
return sendActivity(activityToSend);
|
||||
}
|
||||
|
@ -281,15 +263,14 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> sendActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
throw new IllegalArgumentException("activity");
|
||||
}
|
||||
BotAssert.activityNotNull(activity);
|
||||
|
||||
Activity[] activities = {activity};
|
||||
return sendActivities(activities)
|
||||
return sendActivities(Collections.singletonList(activity))
|
||||
.thenApply(resourceResponses -> {
|
||||
if (resourceResponses == null || resourceResponses.length == 0) {
|
||||
return null;
|
||||
// It's possible an interceptor prevented the activity from having been sent.
|
||||
// Just return an empty response in that case.
|
||||
return new ResourceResponse();
|
||||
}
|
||||
return resourceResponses[0];
|
||||
});
|
||||
|
@ -305,201 +286,104 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
* the receiving channel assigned to the activities.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(Activity[] activities) {
|
||||
// Bind the relevant Conversation Reference properties, such as URLs and
|
||||
// ChannelId's, to the activities we're about to send.
|
||||
ConversationReference cr = getConversationReference(this.activity);
|
||||
for (Activity a : activities) {
|
||||
applyConversationReference(a, cr);
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(List<Activity> activities) {
|
||||
if (activities == null || activities.size() == 0) {
|
||||
throw new IllegalArgumentException("activities");
|
||||
}
|
||||
|
||||
// Convert the IActivities to Activities.
|
||||
List<Activity> activityArray = Arrays.stream(activities).map(input -> input).collect(toList());
|
||||
// Bind the relevant Conversation Reference properties, such as URLs and
|
||||
// ChannelId's, to the activities we're about to send.
|
||||
ConversationReference cr = activity.getConversationReference();
|
||||
|
||||
// Create the list used by the recursive methods.
|
||||
List<Activity> activityList = new ArrayList<Activity>(activityArray);
|
||||
// Buffer the incoming activities into a List<T> since we allow the set to be manipulated by the callbacks
|
||||
// Bind the relevant Conversation Reference properties, such as URLs and
|
||||
// ChannelId's, to the activity we're about to send
|
||||
List<Activity> bufferedActivities = activities.stream()
|
||||
.map(a -> a.applyConversationReference(cr)).collect(Collectors.toList());
|
||||
|
||||
Supplier<CompletableFuture<ResourceResponse[]>> actuallySendStuff = () -> {
|
||||
// Send from the list, which may have been manipulated via the event handlers.
|
||||
// Note that 'responses' was captured from the root of the call, and will be
|
||||
// returned to the original caller.
|
||||
return getAdapter().sendActivities(this, activityList.toArray(new Activity[activityList.size()]))
|
||||
.thenApply(responses -> {
|
||||
if (responses != null && responses.length == activityList.size()) {
|
||||
// stitch up activity ids
|
||||
for (int i = 0; i < responses.length; i++) {
|
||||
ResourceResponse response = responses[i];
|
||||
Activity activity = activityList.get(i);
|
||||
activity.setId(response.getId());
|
||||
}
|
||||
}
|
||||
if (onSendActivities.size() == 0) {
|
||||
return sendActivitiesThroughAdapter(bufferedActivities);
|
||||
}
|
||||
|
||||
// Are the any non-trace activities to send?
|
||||
// The thinking here is that a Trace event isn't user relevant data
|
||||
// so the "Responded" flag should not be set by Trace messages being
|
||||
// sent out.
|
||||
if (activityList.stream().anyMatch((a) -> a.getType() == TRACE)) {
|
||||
this.setResponded(true);
|
||||
}
|
||||
return responses;
|
||||
});
|
||||
};
|
||||
return sendActivitiesThroughCallbackPipeline(bufferedActivities, 0);
|
||||
}
|
||||
|
||||
List<Activity> act_list = new ArrayList<>(activityList);
|
||||
return sendActivitiesInternal(act_list, onSendActivities.iterator(), actuallySendStuff);
|
||||
private CompletableFuture<ResourceResponse[]> sendActivitiesThroughAdapter(List<Activity> activities) {
|
||||
return adapter.sendActivities(this, activities)
|
||||
.thenApply(responses -> {
|
||||
boolean sentNonTraceActivity = false;
|
||||
|
||||
for (int index = 0; index < responses.length; index++) {
|
||||
Activity activity = activities.get(index);
|
||||
activity.setId(responses[index].getId());
|
||||
sentNonTraceActivity |= !activity.isType(ActivityTypes.TRACE);
|
||||
}
|
||||
|
||||
if (sentNonTraceActivity) {
|
||||
responded = true;
|
||||
}
|
||||
|
||||
return responses;
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<ResourceResponse[]> sendActivitiesThroughCallbackPipeline(List<Activity> activities,
|
||||
int nextCallbackIndex) {
|
||||
|
||||
if (nextCallbackIndex == onSendActivities.size()) {
|
||||
return sendActivitiesThroughAdapter(activities);
|
||||
}
|
||||
|
||||
return onSendActivities.get(nextCallbackIndex).invoke(this,
|
||||
activities, () -> sendActivitiesThroughCallbackPipeline(activities, nextCallbackIndex + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an existing activity.
|
||||
*
|
||||
* @param activity New replacement activity.
|
||||
* @param withActivity New replacement activity.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the response contained additional information.
|
||||
* @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the
|
||||
* response contained additional information.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse> updateActivity(Activity activity) {
|
||||
Supplier<CompletableFuture<ResourceResponse>> ActuallyUpdateStuff = () -> {
|
||||
return getAdapter().updateActivity(this, activity);
|
||||
};
|
||||
|
||||
return updateActivityInternal(activity, onUpdateActivity.iterator(), ActuallyUpdateStuff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing activity.
|
||||
*
|
||||
* @param activityId The ID of the activity to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws Exception The HTTP operation failed and the response contained additional information.
|
||||
*/
|
||||
public CompletableFuture<Void> deleteActivity(String activityId) {
|
||||
if (StringUtils.isWhitespace(activityId) || activityId == null) {
|
||||
throw new IllegalArgumentException("activityId");
|
||||
}
|
||||
|
||||
ConversationReference cr = getConversationReference(getActivity());
|
||||
cr.setActivityId(activityId);
|
||||
|
||||
Supplier<CompletableFuture<Void>> ActuallyDeleteStuff = () ->
|
||||
getAdapter().deleteActivity(this, cr);
|
||||
|
||||
return deleteActivityInternal(cr, onDeleteActivity.iterator(), ActuallyDeleteStuff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing activity.
|
||||
*
|
||||
* @param conversationReference The conversation containing the activity to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
* @throws com.microsoft.bot.connector.rest.ErrorResponseException The HTTP operation failed and the response contained additional information.
|
||||
* The conversation reference's {@link ConversationReference#getActivityId}
|
||||
* indicates the activity in the conversation to delete.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Void> deleteActivity(ConversationReference conversationReference) {
|
||||
if (conversationReference == null)
|
||||
throw new IllegalArgumentException("conversationReference");
|
||||
|
||||
Supplier<CompletableFuture<Void>> ActuallyDeleteStuff = () ->
|
||||
getAdapter().deleteActivity(this, conversationReference);
|
||||
|
||||
return deleteActivityInternal(conversationReference, onDeleteActivity.iterator(), ActuallyDeleteStuff);
|
||||
}
|
||||
|
||||
private CompletableFuture<ResourceResponse[]> sendActivitiesInternal(
|
||||
List<Activity> activities,
|
||||
Iterator<SendActivitiesHandler> sendHandlers,
|
||||
Supplier<CompletableFuture<ResourceResponse[]>> callAtBottom) {
|
||||
|
||||
if (activities == null) {
|
||||
throw new IllegalArgumentException("activities");
|
||||
}
|
||||
if (sendHandlers == null) {
|
||||
throw new IllegalArgumentException("sendHandlers");
|
||||
}
|
||||
|
||||
if (!sendHandlers.hasNext()) { // No middleware to run.
|
||||
if (callAtBottom != null) {
|
||||
return callAtBottom.get();
|
||||
}
|
||||
return CompletableFuture.completedFuture(new ResourceResponse[0]);
|
||||
}
|
||||
|
||||
// Default to "No more Middleware after this".
|
||||
Supplier<CompletableFuture<ResourceResponse[]>> next = () -> {
|
||||
// Remove the first item from the list of middleware to call,
|
||||
// so that the next call just has the remaining items to worry about.
|
||||
//Iterable<SendActivitiesHandler> remaining = sendHandlers.Skip(1);
|
||||
//Iterator<SendActivitiesHandler> remaining = sendHandlers.iterator();
|
||||
if (sendHandlers.hasNext())
|
||||
sendHandlers.next();
|
||||
return sendActivitiesInternal(activities, sendHandlers, callAtBottom);
|
||||
};
|
||||
|
||||
// Grab the current middleware, which is the 1st element in the array, and execute it
|
||||
SendActivitiesHandler caller = sendHandlers.next();
|
||||
return caller.invoke(this, activities, next);
|
||||
}
|
||||
|
||||
// private async Task<ResourceResponse> UpdateActivityInternal(Activity activity,
|
||||
// IEnumerable<UpdateActivityHandler> updateHandlers,
|
||||
// Func<Task<ResourceResponse>> callAtBottom)
|
||||
// {
|
||||
// BotAssert.ActivityNotNull(activity);
|
||||
// if (updateHandlers == null)
|
||||
// throw new ArgumentException(nameof(updateHandlers));
|
||||
//
|
||||
// if (updateHandlers.Count() == 0) // No middleware to run.
|
||||
// {
|
||||
// if (callAtBottom != null)
|
||||
// {
|
||||
// return await callAtBottom();
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// */ Default to "No more Middleware after this".
|
||||
// */
|
||||
// async Task<ResourceResponse> next()
|
||||
// {
|
||||
// /**
|
||||
// */ Remove the first item from the list of middleware to call,
|
||||
// */ so that the next call just has the remaining items to worry about.
|
||||
// */
|
||||
// IEnumerable<UpdateActivityHandler> remaining = updateHandlers.Skip(1);
|
||||
// var result = await UpdateActivityInternal(activity, remaining, callAtBottom).ConfigureAwait(false);
|
||||
// activity.Id = result.Id;
|
||||
// return result;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// */ Grab the current middleware, which is the 1st element in the array, and execute it
|
||||
// */
|
||||
// UpdateActivityHandler toCall = updateHandlers.First();
|
||||
// return await toCall(this, activity, next);
|
||||
// }
|
||||
private CompletableFuture<ResourceResponse> updateActivityInternal(Activity activity,
|
||||
Iterator<UpdateActivityHandler> updateHandlers,
|
||||
Supplier<CompletableFuture<ResourceResponse>> callAtBottom) {
|
||||
public CompletableFuture<ResourceResponse> updateActivity(Activity withActivity) {
|
||||
BotAssert.activityNotNull(activity);
|
||||
if (updateHandlers == null)
|
||||
throw new IllegalArgumentException("updateHandlers");
|
||||
|
||||
if (false == updateHandlers.hasNext()) { // No middleware to run.
|
||||
ConversationReference conversationReference = activity.getConversationReference();
|
||||
withActivity.applyConversationReference(conversationReference);
|
||||
|
||||
Supplier<CompletableFuture<ResourceResponse>> actuallyUpdateStuff =
|
||||
() -> getAdapter().updateActivity(this, withActivity);
|
||||
|
||||
return updateActivityInternal(withActivity, onUpdateActivity.iterator(), actuallyUpdateStuff);
|
||||
}
|
||||
|
||||
private CompletableFuture<ResourceResponse> updateActivityInternal(
|
||||
Activity activity,
|
||||
Iterator<UpdateActivityHandler> updateHandlers,
|
||||
Supplier<CompletableFuture<ResourceResponse>> callAtBottom) {
|
||||
|
||||
BotAssert.activityNotNull(activity);
|
||||
if (updateHandlers == null) {
|
||||
throw new IllegalArgumentException("updateHandlers");
|
||||
}
|
||||
|
||||
// No middleware to run.
|
||||
if (!updateHandlers.hasNext()) {
|
||||
if (callAtBottom != null) {
|
||||
return callAtBottom.get();
|
||||
}
|
||||
return null;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// Default to "No more Middleware after this".
|
||||
Supplier<CompletableFuture<ResourceResponse>> next = () -> {
|
||||
// Remove the first item from the list of middleware to call,
|
||||
// so that the next call just has the remaining items to worry about.
|
||||
if (updateHandlers.hasNext())
|
||||
if (updateHandlers.hasNext()) {
|
||||
updateHandlers.next();
|
||||
}
|
||||
|
||||
return updateActivityInternal(activity, updateHandlers, callAtBottom)
|
||||
.thenApply(resourceResponse -> {
|
||||
|
@ -513,14 +397,57 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
return toCall.invoke(this, activity, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing activity.
|
||||
*
|
||||
* @param activityId The ID of the activity to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
public CompletableFuture<Void> deleteActivity(String activityId) {
|
||||
if (StringUtils.isWhitespace(activityId) || StringUtils.isEmpty(activityId)) {
|
||||
throw new IllegalArgumentException("activityId");
|
||||
}
|
||||
|
||||
ConversationReference cr = activity.getConversationReference();
|
||||
cr.setActivityId(activityId);
|
||||
|
||||
Supplier<CompletableFuture<Void>> actuallyDeleteStuff = () ->
|
||||
getAdapter().deleteActivity(this, cr);
|
||||
|
||||
return deleteActivityInternal(cr, onDeleteActivity.iterator(), actuallyDeleteStuff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing activity.
|
||||
*
|
||||
* The conversation reference's {@link ConversationReference#getActivityId}
|
||||
* indicates the activity in the conversation to delete.
|
||||
*
|
||||
* @param conversationReference The conversation containing the activity to delete.
|
||||
* @return A task that represents the work queued to execute.
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Void> deleteActivity(ConversationReference conversationReference) {
|
||||
if (conversationReference == null) {
|
||||
throw new IllegalArgumentException("conversationReference");
|
||||
}
|
||||
|
||||
Supplier<CompletableFuture<Void>> actuallyDeleteStuff = () ->
|
||||
getAdapter().deleteActivity(this, conversationReference);
|
||||
|
||||
return deleteActivityInternal(conversationReference, onDeleteActivity.iterator(), actuallyDeleteStuff);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> deleteActivityInternal(ConversationReference cr,
|
||||
Iterator<DeleteActivityHandler> deleteHandlers,
|
||||
Supplier<CompletableFuture<Void>> callAtBottom) {
|
||||
BotAssert.conversationReferenceNotNull(cr);
|
||||
if (deleteHandlers == null)
|
||||
if (deleteHandlers == null) {
|
||||
throw new IllegalArgumentException("deleteHandlers");
|
||||
}
|
||||
|
||||
if (!deleteHandlers.hasNext()) { // No middleware to run.
|
||||
// No middleware to run.
|
||||
if (!deleteHandlers.hasNext()) {
|
||||
if (callAtBottom != null) {
|
||||
return callAtBottom.get();
|
||||
}
|
||||
|
@ -531,10 +458,9 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
Supplier<CompletableFuture<Void>> next = () -> {
|
||||
// Remove the first item from the list of middleware to call,
|
||||
// so that the next call just has the remaining items to worry about.
|
||||
|
||||
//Iterator<UpdateActivityHandler> remaining = (deleteHandlers.hasNext()) ? deleteHandlers.next() : null;
|
||||
if (deleteHandlers.hasNext())
|
||||
if (deleteHandlers.hasNext()) {
|
||||
deleteHandlers.next();
|
||||
}
|
||||
|
||||
return deleteActivityInternal(cr, deleteHandlers, callAtBottom);
|
||||
};
|
||||
|
@ -544,6 +470,16 @@ public class TurnContextImpl implements TurnContext, AutoCloseable {
|
|||
return toCall.invoke(this, cr, next);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalize() {
|
||||
try {
|
||||
close();
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
turnState.close();
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package com.microsoft.bot.builder;
|
||||
|
||||
import com.microsoft.bot.connector.ConnectorClient;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -71,6 +73,9 @@ public class TurnContextStateCollection extends HashMap<String, Object> implemen
|
|||
public void close() throws Exception {
|
||||
for (Map.Entry entry : entrySet()) {
|
||||
if (entry.getValue() instanceof AutoCloseable) {
|
||||
if (entry.getValue() instanceof ConnectorClient) {
|
||||
continue;
|
||||
}
|
||||
((AutoCloseable) entry.getValue()).close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -279,7 +279,7 @@ public class ActivityHandlerTests {
|
|||
|
||||
private static class NotImplementedAdapter extends BotAdapter {
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, Activity[] activities) {
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, List<Activity> activities) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -28,7 +29,7 @@ public class BotAdapterTests {
|
|||
|
||||
@Test
|
||||
public void PassResourceResponsesThrough() {
|
||||
Consumer<Activity[]> validateResponse = (activities) -> {
|
||||
Consumer<List<Activity>> validateResponse = (activities) -> {
|
||||
// no need to do anything.
|
||||
};
|
||||
|
||||
|
|
|
@ -14,20 +14,20 @@ import java.util.concurrent.CompletableFuture;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
public class SimpleAdapter extends BotAdapter {
|
||||
private Consumer<Activity[]> callOnSend = null;
|
||||
private Consumer<List<Activity>> callOnSend = null;
|
||||
private Consumer<Activity> callOnUpdate = null;
|
||||
private Consumer<ConversationReference> callOnDelete = null;
|
||||
|
||||
// Callback Function but doesn't need to be. Avoid java legacy type erasure
|
||||
public SimpleAdapter(Consumer<Activity[]> callOnSend) {
|
||||
public SimpleAdapter(Consumer<List<Activity>> callOnSend) {
|
||||
this(callOnSend, null, null);
|
||||
}
|
||||
|
||||
public SimpleAdapter(Consumer<Activity[]> callOnSend, Consumer<Activity> callOnUpdate) {
|
||||
public SimpleAdapter(Consumer<List<Activity>> callOnSend, Consumer<Activity> callOnUpdate) {
|
||||
this(callOnSend, callOnUpdate, null);
|
||||
}
|
||||
|
||||
public SimpleAdapter(Consumer<Activity[]> callOnSend, Consumer<Activity> callOnUpdate, Consumer<ConversationReference> callOnDelete) {
|
||||
public SimpleAdapter(Consumer<List<Activity>> callOnSend, Consumer<Activity> callOnUpdate, Consumer<ConversationReference> callOnDelete) {
|
||||
this.callOnSend = callOnSend;
|
||||
this.callOnUpdate = callOnUpdate;
|
||||
this.callOnDelete = callOnDelete;
|
||||
|
@ -39,9 +39,9 @@ public class SimpleAdapter extends BotAdapter {
|
|||
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, Activity[] activities) {
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, List<Activity> activities) {
|
||||
Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", activities);
|
||||
Assert.assertTrue("SimpleAdapter.sendActivities: empty activities array.", activities.length > 0);
|
||||
Assert.assertTrue("SimpleAdapter.sendActivities: empty activities array.", activities.size() > 0);
|
||||
|
||||
if (this.callOnSend != null)
|
||||
this.callOnSend.accept(activities);
|
||||
|
@ -67,7 +67,7 @@ public class SimpleAdapter extends BotAdapter {
|
|||
Assert.assertNotNull("SimpleAdapter.deleteActivity: missing reference", reference);
|
||||
if (callOnDelete != null)
|
||||
this.callOnDelete.accept(reference);
|
||||
return null;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -91,7 +91,7 @@ public class TestAdapter extends BotAdapter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, Activity[] activities) {
|
||||
public CompletableFuture<ResourceResponse[]> sendActivities(TurnContext context, List<Activity> activities) {
|
||||
List<ResourceResponse> responses = new LinkedList<ResourceResponse>();
|
||||
|
||||
for (Activity activity : activities) {
|
||||
|
@ -104,7 +104,7 @@ public class TestAdapter extends BotAdapter {
|
|||
responses.add(new ResourceResponse(activity.getId()));
|
||||
// This is simulating DELAY
|
||||
|
||||
System.out.println(String.format("TestAdapter:SendActivities(tid:%s):Count:%s", Thread.currentThread().getId(), activities.length));
|
||||
System.out.println(String.format("TestAdapter:SendActivities(tid:%s):Count:%s", Thread.currentThread().getId(), activities.size()));
|
||||
for (Activity act : activities) {
|
||||
System.out.printf(":--------\n: To:%s\n", act.getRecipient().getName());
|
||||
System.out.printf(": From:%s\n", (act.getFrom() == null) ? "No from set" : act.getFrom().getName());
|
||||
|
|
|
@ -16,7 +16,7 @@ import com.microsoft.rest.RestClient;
|
|||
/**
|
||||
* The interface for ConnectorClient class.
|
||||
*/
|
||||
public interface ConnectorClient {
|
||||
public interface ConnectorClient extends AutoCloseable {
|
||||
/**
|
||||
* Gets the REST client.
|
||||
*
|
||||
|
@ -94,4 +94,9 @@ public interface ConnectorClient {
|
|||
* @return the Conversations object.
|
||||
*/
|
||||
Conversations getConversations();
|
||||
|
||||
@Override
|
||||
default void close() throws Exception {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,10 @@ package com.microsoft.bot.connector.rest;
|
|||
import com.microsoft.azure.AzureClient;
|
||||
import com.microsoft.azure.AzureResponseBuilder;
|
||||
import com.microsoft.azure.AzureServiceClient;
|
||||
import com.microsoft.bot.connector.*;
|
||||
import com.microsoft.bot.connector.Attachments;
|
||||
import com.microsoft.bot.connector.ConnectorClient;
|
||||
import com.microsoft.bot.connector.Conversations;
|
||||
import com.microsoft.bot.connector.UserAgent;
|
||||
import com.microsoft.rest.credentials.ServiceClientCredentials;
|
||||
import com.microsoft.rest.RestClient;
|
||||
import com.microsoft.rest.retry.RetryStrategy;
|
||||
|
|
|
@ -313,11 +313,21 @@ public class Activity {
|
|||
/**
|
||||
* Create a TRACE type Activity.
|
||||
*
|
||||
* @param withName Name of the operation
|
||||
* @param withValueType valueType if helpful to identify the value schema (default is value.GetType().Name)
|
||||
* @param withValue The content for this trace operation.
|
||||
* @param withLabel A descriptive label for this trace operation.
|
||||
* @param withName Name of the operation
|
||||
*/
|
||||
public static Activity createTraceActivity(String withName) {
|
||||
return createTraceActivity(withName, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a TRACE type Activity.
|
||||
*
|
||||
* @param withName Name of the operation
|
||||
* @param withValueType valueType if helpful to identify the value schema (default is value.GetType().Name)
|
||||
* @param withValue The content for this trace operation.
|
||||
* @param withLabel A descriptive label for this trace operation.
|
||||
*/
|
||||
public static Activity createTraceActivity(String withName,
|
||||
String withValueType,
|
||||
Object withValue,
|
||||
|
@ -325,7 +335,11 @@ public class Activity {
|
|||
return new Activity(ActivityTypes.TRACE) {{
|
||||
setName(withName);
|
||||
setLabel(withLabel);
|
||||
setValueType((withValueType == null) ? withValue.getClass().getTypeName() : withValueType);
|
||||
if (withValue != null) {
|
||||
setValueType((withValueType == null) ? withValue.getClass().getTypeName() : withValueType);
|
||||
} else {
|
||||
setValueType(withValueType);
|
||||
}
|
||||
setValue(withValue);
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
|
||||
package com.microsoft.bot.schema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Defines values for ActivityTypes.
|
||||
*/
|
||||
|
|
Загрузка…
Ссылка в новой задаче