Skip to content

Commit

Permalink
feat(core): implements new chat processor
Browse files Browse the repository at this point in the history
  • Loading branch information
Siroshun09 committed Apr 9, 2024
1 parent ece6e00 commit 2d627e7
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package net.okocraft.okochat.core.chat.context;

import net.kyori.adventure.text.Component;
import net.okocraft.okochat.api.channel.Channel;
import net.okocraft.okochat.api.chat.context.ChannelChatContext;
import net.okocraft.okochat.api.sender.Sender;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;

public final class ChannelChatContexts {

public record Preparing(@NotNull Sender sender, @NotNull String message,
@NotNull Channel channel) implements ChannelChatContext {
}

public record Formatted(@NotNull Sender sender, @NotNull String message, @NotNull Channel channel,
@NotNull Component formattedMessage) implements ChannelChatContext {
}

public record Ready(@NotNull Sender sender, @NotNull String message, @NotNull Channel channel,
@NotNull Component formattedMessage,
@NotNull Collection<Sender> recipients) implements ChannelChatContext {
}

private ChannelChatContexts() {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.okocraft.okochat.core.chat.context;

import net.kyori.adventure.text.Component;
import net.okocraft.okochat.api.chat.context.PrivateChatContext;
import net.okocraft.okochat.api.sender.Sender;
import org.jetbrains.annotations.NotNull;

public final class PrivateChatContexts {

public record Preparing(@NotNull Sender sender, @NotNull String message,
@NotNull Sender target) implements PrivateChatContext {
}

public record Ready(@NotNull Sender sender, @NotNull String message, @NotNull Sender target,
@NotNull Component formattedMessage) implements PrivateChatContext {
}

private PrivateChatContexts() {
throw new UnsupportedOperationException();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package net.okocraft.okochat.core.chat.processor;

import com.github.siroshun09.event4j.caller.EventCaller;
import net.okocraft.okochat.api.channel.Channel;
import net.okocraft.okochat.api.channel.member.status.MemberStatus;
import net.okocraft.okochat.api.chat.context.ChannelChatContext;
import net.okocraft.okochat.api.chat.converter.ChatConverter;
import net.okocraft.okochat.api.chat.ngword.DetectedNGWord;
import net.okocraft.okochat.api.chat.ngword.NGWord;
import net.okocraft.okochat.api.chat.result.ChannelChatResult;
import net.okocraft.okochat.api.event.LunaChatEvent;
import net.okocraft.okochat.api.event.chat.channelchat.ChannelFindEvent;
import net.okocraft.okochat.api.event.chat.channelchat.PostChannelChatEvent;
import net.okocraft.okochat.api.event.chat.channelchat.PreChannelChatEvent;
import net.okocraft.okochat.api.identity.Identified;
import net.okocraft.okochat.api.sender.Sender;
import net.okocraft.okochat.api.util.either.Either;
import net.okocraft.okochat.core.chat.context.ChannelChatContexts;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.function.Function;
import java.util.regex.Pattern;

public record ChannelChatProcessor(
@Nullable String globalChannelMarker,
@Nullable Channel globalChannel,
@Nullable Pattern quickChatSeparatorPattern,
@NotNull Function<String, Channel> channelAccessor,
@NotNull Function<Sender, Channel> defaultChannelProvider,
@NotNull EventCaller<? super LunaChatEvent> eventCaller,
@NotNull Function<Sender, ChatConverter> converterAccessor,
@NotNull String disableChatConvertMarker,
@NotNull Collection<NGWord> ngWords
) implements ChatProcessor<ProcessContext.ChannelChat, ChannelChatContext, ChannelChatResult> {

@Override
public @NotNull ChannelChatResult process(@NotNull ProcessContext.ChannelChat context) {
return this.findChannel(context)
.flatMap(this::callPreChannelChatEvent)
.flatMap(this::checkPermission)
.flatMap(this::checkMuted)
.map(this::convertChatMessageIfNeeded)
.flatMap(this::detectNGWords)
.map(this::formatChannelChat)
.map(this::collectRecipients)
.map(this::sendMessage)
.map(this::callPostChannelChatEvent)
.fold(Function.identity(), this::success);
}

@Override
public @NotNull ChannelChatResult onNonMaskableNGWordDetected(@NotNull ChannelChatContext context, @NotNull DetectedNGWord detectedNGWord) {
return new ChannelChatResult.NGWord(context.sender(), context.message(), context.channel(), detectedNGWord);
}

@Override
public @NotNull ChannelChatContext createContextWithNewMessage(@NotNull ChannelChatContext context, @NotNull String newMessage) {
return new ChannelChatContexts.Preparing(context.sender(), newMessage, context.channel());
}

private @NotNull Either<ChannelChatResult, ChannelChatContext> findChannel(@NotNull ProcessContext.ChannelChat processContext) {
return Either.<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>>left(processContext)
.flatMapLeft(this::useSpecifiedChannelIfPossible)
.flatMapLeft(this::findGlobalChatMarker)
.flatMapLeft(this::findQuickChannelMarker)
.flatMapLeft(this::findDefaultChannel)
.flatMapLeft(this::fallbackToGlobalChannel)
.mapLeft(data -> (ChannelChatResult) new ChannelChatResult.ChannelNotFound(data.sender(), data.message(), null))
.flatMap(either -> either.isRight() ? this.callChannelFindEvent(processContext, either.right()) : either);
}

private @NotNull Either<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>> useSpecifiedChannelIfPossible(@NotNull ProcessContext.ChannelChat context) {
return context.channel() != null ? Either.right(Either.right(new ChannelChatContexts.Preparing(context.sender(), context.message(), context.channel()))) : Either.left(context);
}

private @NotNull Either<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>> findGlobalChatMarker(ProcessContext.ChannelChat processContext) {
if (this.globalChannelMarker == null || this.globalChannel == null) {
return Either.left(processContext);
}

int markerLength = this.globalChannelMarker.length();
var message = processContext.message();

return markerLength != 0 && markerLength < message.length() && message.startsWith(this.globalChannelMarker) ?
Either.right(channelFound(processContext.sender(), message.substring(markerLength), this.globalChannel)) :
Either.left(processContext);
}

private @NotNull Either<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>> findQuickChannelMarker(@NotNull ProcessContext.ChannelChat processContext) {
if (this.quickChatSeparatorPattern == null) {
return Either.left(processContext);
}

var message = processContext.message();
var parts = this.quickChatSeparatorPattern.split(message, 2);

if (parts.length == 2 && !parts[1].isEmpty()) {
var channel = this.channelAccessor.apply(parts[0]);
return Either.right(
channel != null ?
Either.right(new ChannelChatContexts.Preparing(processContext.sender(), parts[1], channel)) :
Either.left(new ChannelChatResult.ChannelNotFound(processContext.sender(), parts[1], parts[0]))
);
} else {
return Either.left(processContext);
}
}

private @NotNull Either<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>> findDefaultChannel(@NotNull ProcessContext.ChannelChat processContext) {
var defaultChannel = this.defaultChannelProvider.apply(processContext.sender());
return defaultChannel != null ?
Either.right(channelFound(processContext.sender(), processContext.message(), defaultChannel)) :
Either.left(processContext);
}

private @NotNull Either<ProcessContext.ChannelChat, Either<ChannelChatResult, ChannelChatContext>> fallbackToGlobalChannel(@NotNull ProcessContext.ChannelChat processContext) {
return this.globalChannel != null ?
Either.right(Either.right(new ChannelChatContexts.Preparing(processContext.sender(), processContext.message(), this.globalChannel))) :
Either.left(processContext);
}

private @NotNull Either<ChannelChatResult, ChannelChatContext> callChannelFindEvent(@NotNull ProcessContext.ChannelChat processContext, @NotNull ChannelChatContext context) {
var event = new ChannelFindEvent(context.channel(), context.sender(), processContext.message(), context.message());
this.eventCaller.call(event);
return event.isCancelled() ?
Either.left(new ChannelChatResult.ChannelNotFound(context.sender(), processContext.message(), null)) :
Either.right(new ChannelChatContexts.Preparing(event.getSender(), event.getResultChatMessage(), event.getResultChannel()));
}

private static @NotNull Either<ChannelChatResult, ChannelChatContext> channelFound(@NotNull Sender sender, @NotNull String message, @NotNull Channel channel) {
return Either.right(new ChannelChatContexts.Preparing(sender, message, channel));
}

private @NotNull Either<ChannelChatResult, ChannelChatContext> callPreChannelChatEvent(@NotNull ChannelChatContext context) {
var event = new PreChannelChatEvent(context.channel(), context.sender(), context.message());
this.eventCaller.call(event);
return event.isCancelled() ?
Either.left(new ChannelChatResult.EventCancelled(event.getSender(), event.getOriginalMessage(), event.getChannel())) :
Either.right(new ChannelChatContexts.Preparing(event.getSender(), event.getResultMessage(), event.getChannel()));
}

private @NotNull Either<ChannelChatResult, ChannelChatContext> checkPermission(@NotNull ChannelChatContext context) {
var permission = context.channel().getSpeakPermission();
return context.sender().hasPermission(permission.node(), permission.defaultValue()) ?
Either.right(context) :
Either.left(new ChannelChatResult.NoPermission(context.sender(), context.message(), context.channel(), permission));
}

private @NotNull Either<ChannelChatResult, ChannelChatContext> checkMuted(@NotNull ChannelChatContext context) {
return context.sender() instanceof Identified identified && context.channel().getMemberHolder().getStatus(identified.identity()) instanceof MemberStatus.Muted ?
Either.left(new ChannelChatResult.Muted(context.sender(), context.message(), context.channel())) :
Either.right(context);
}

private @NotNull ChannelChatContexts.Formatted formatChannelChat(@NotNull ChannelChatContext context) {
return new ChannelChatContexts.Formatted(context.sender(), context.message(), context.channel(), context.channel().getChatMessageFormat().render(context));
}

private @NotNull ChannelChatContexts.Ready collectRecipients(@NotNull ChannelChatContexts.Formatted context) {
// TODO
return new ChannelChatContexts.Ready(context.sender(), context.message(), context.channel(), context.formattedMessage(), Collections.emptyList());
}

private @NotNull ChannelChatContexts.Ready sendMessage(@NotNull ChannelChatContexts.Ready context) {
context.recipients().forEach(recipient -> recipient.sendMessage(context.formattedMessage())); // TODO: event?
return context;
}

private @NotNull ChannelChatContexts.Ready callPostChannelChatEvent(@NotNull ChannelChatContexts.Ready context) {
this.eventCaller.call(new PostChannelChatEvent(context.channel(), context.sender(), context.message(), context.formattedMessage(), context.recipients()));
return context;
}

private @NotNull ChannelChatResult success(@NotNull ChannelChatContexts.Ready context) {
return new ChannelChatResult.Success(context.sender(), context.message(), context.channel(), context.formattedMessage(), context.recipients());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package net.okocraft.okochat.core.chat.processor;

import net.okocraft.okochat.api.chat.context.ChatContext;
import net.okocraft.okochat.api.chat.converter.ChatConverter;
import net.okocraft.okochat.api.sender.Sender;
import net.okocraft.okochat.api.util.either.Either;
import net.okocraft.okochat.api.chat.ngword.DetectedNGWord;
import net.okocraft.okochat.api.chat.ngword.NGWord;
import net.okocraft.okochat.api.chat.ngword.NGWordOperationType;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.function.Function;

public interface ChatProcessor<PC extends ProcessContext, C extends ChatContext, R> {

@NotNull R process(@NotNull PC chatData);

@NotNull Function<Sender, ChatConverter> converterAccessor();

@NotNull String disableChatConvertMarker();

@NotNull Collection<NGWord> ngWords();

@NotNull R onNonMaskableNGWordDetected(@NotNull C context, @NotNull DetectedNGWord detectedNGWord);

@NotNull C createContextWithNewMessage(@NotNull C context, @NotNull String newMessage);

default @NotNull C convertChatMessageIfNeeded(@NotNull C context) {
var converter = this.converterAccessor().apply(context.sender());

if (converter == ChatConverter.NOOP) {
return context;
}

var disableMarker = this.disableChatConvertMarker();

if (!disableMarker.isEmpty() && context.message().startsWith(disableMarker)) {
return context.message().equals(disableMarker) ?
context :
this.createContextWithNewMessage(context, context.message().substring(disableMarker.length()));
}

return this.createContextWithNewMessage(context, converter.convert(context.message()));
}

default @NotNull Either<R, C> detectNGWords(@NotNull C context) {
var currentMessage = context.message();
boolean masked = false;

for (var ngWord : this.ngWords()) {
var result = ngWord.detect(currentMessage);
if (result.isPresent()) {
var detected = result.get();
if (detected.ngWord().operationType() == NGWordOperationType.MASK) {
currentMessage = detected.maskedMessage().orElseThrow();
masked = true;
} else {
return Either.left(this.onNonMaskableNGWordDetected(context, detected));
}
}
}

return Either.right(masked ? this.createContextWithNewMessage(context, currentMessage) : context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package net.okocraft.okochat.core.chat.processor;

import com.github.siroshun09.event4j.caller.EventCaller;
import net.okocraft.okochat.api.chat.context.PrivateChatContext;
import net.okocraft.okochat.api.chat.format.ChatMessageFormat;
import net.okocraft.okochat.api.chat.converter.ChatConverter;
import net.okocraft.okochat.api.event.LunaChatEvent;
import net.okocraft.okochat.api.event.chat.privatechat.PostPrivateChatEvent;
import net.okocraft.okochat.api.event.chat.privatechat.PrePrivateChatEvent;
import net.okocraft.okochat.api.sender.Sender;
import net.okocraft.okochat.api.util.either.Either;
import net.okocraft.okochat.core.chat.context.PrivateChatContexts;
import net.okocraft.okochat.api.chat.result.PrivateChatResult;
import net.okocraft.okochat.api.chat.ngword.DetectedNGWord;
import net.okocraft.okochat.api.chat.ngword.NGWord;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.function.Function;

public record PrivateChatProcessor(
@NotNull EventCaller<? super LunaChatEvent> eventCaller,
@NotNull ChatMessageFormat<PrivateChatContext> chatMessageFormat,
@NotNull Function<Sender, ChatConverter> converterAccessor,
@NotNull String disableChatConvertMarker,
@NotNull Collection<NGWord> ngWords
) implements ChatProcessor<ProcessContext.PrivateChat, PrivateChatContext, PrivateChatResult> {

@Override
public @NotNull PrivateChatResult process(@NotNull ProcessContext.PrivateChat context) {
return this.toPrivateChatContext(context)
.flatMap(this::callPrePrivateChatEvent)
.flatMap(this::checkMuted)
.map(this::convertChatMessageIfNeeded)
.flatMap(this::detectNGWords)
.map(this::formatPrivateChat)
.map(this::sendMessage)
.map(this::callPostPrivateChatEvent)
.fold(Function.identity(), this::success);
}

@Override
public @NotNull PrivateChatResult onNonMaskableNGWordDetected(@NotNull PrivateChatContext context, @NotNull DetectedNGWord detectedNGWord) {
return new PrivateChatResult.NGWord(context.sender(), context.message(), context.target(), detectedNGWord);
}

@Override
public @NotNull PrivateChatContext createContextWithNewMessage(@NotNull PrivateChatContext context, @NotNull String newMessage) {
return new PrivateChatContexts.Preparing(context.sender(), newMessage, context.target());
}

private @NotNull Either<PrivateChatResult, PrivateChatContext> toPrivateChatContext(@NotNull ProcessContext.PrivateChat processContext) {
return Either.right(new PrivateChatContexts.Preparing(processContext.sender(), processContext.message(), processContext.target()));
}

private @NotNull Either<PrivateChatResult, PrivateChatContext> callPrePrivateChatEvent(@NotNull PrivateChatContext context) {
var event = new PrePrivateChatEvent(context.sender(), context.target(), context.message());
this.eventCaller.call(event);
return event.isCancelled() ?
Either.left(new PrivateChatResult.EventCancelled(event.getSender(), event.getOriginalMessage(), event.getTarget())) :
Either.right(new PrivateChatContexts.Preparing(event.getSender(), event.getResultMessage(), event.getTarget()));
}

private @NotNull Either<PrivateChatResult, PrivateChatContext> checkMuted(@NotNull PrivateChatContext context) { // TODO
return Either.right(context);
}

private @NotNull PrivateChatContexts.Ready formatPrivateChat(@NotNull PrivateChatContext context) {
return new PrivateChatContexts.Ready(context.sender(), context.message(), context.target(), this.chatMessageFormat.render(context));
}

private @NotNull PrivateChatContexts.Ready callPostPrivateChatEvent(@NotNull PrivateChatContexts.Ready context) {
this.eventCaller.call(new PostPrivateChatEvent(context.sender(), context.target(), context.message(), context.formattedMessage()));
return context;
}

private @NotNull PrivateChatContexts.Ready sendMessage(@NotNull PrivateChatContexts.Ready context) {
context.sender().sendMessage(context.formattedMessage());
context.target().sendMessage(context.formattedMessage());
return context;
}

private @NotNull PrivateChatResult success(@NotNull PrivateChatContexts.Ready context) {
return new PrivateChatResult.Success(context.sender(), context.message(), context.target(), context.formattedMessage());
}
}
Loading

0 comments on commit 2d627e7

Please sign in to comment.