Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start adding some functionality to Mask #158

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.kitteh.irc.client.library.element.mode.ModeStatus;
import org.kitteh.irc.client.library.element.mode.ModeStatusList;
import org.kitteh.irc.client.library.util.CIKeyMap;
import org.kitteh.irc.client.library.util.Mask;
import org.kitteh.irc.client.library.util.Sanity;
import org.kitteh.irc.client.library.util.ToStringer;

Expand Down Expand Up @@ -746,13 +747,7 @@ public String toString() {
}
}

// Valid nick chars: \w\[]^`{}|-_
// Pattern unescaped: ([\w\\\[\]\^`\{\}\|\-_]+)!([~\w]+)@([\w\.\-:]+)
// You know what? Screw it.
// Let's just do it assuming no IRCD can handle following the rules.
// New pattern: ([^!@]+)!([^!@]+)@([^!@]+)
private static final Pattern NICK_PATTERN = Pattern.compile("([^!@]+)!([^!@]+)@([^!@]+)");
private static final Pattern SERVER_PATTERN = Pattern.compile("(?!-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,126}(?!\\d+)[a-zA-Z\\d]{1,63}");
private static final Pattern SERVER_PATTERN = Pattern.compile("(?!\\-)(?:[a-zA-Z\\d\\-]{0,62}[a-zA-Z\\d]\\.){1,126}(?!\\d+)[a-zA-Z\\d]{1,63}");

private final InternalClient client;

Expand Down Expand Up @@ -783,7 +778,7 @@ void unTrackChannel(@Nonnull IRCChannel channel) {

@Nonnull
IRCActor getActor(@Nonnull String name) {
Matcher nickMatcher = NICK_PATTERN.matcher(name);
Matcher nickMatcher = Mask.NICK_PATTERN.matcher(name);
if (nickMatcher.matches()) {
String nick = nickMatcher.group(1);
IRCUser user = this.trackedUsers.get(nick);
Expand Down
187 changes: 178 additions & 9 deletions src/main/java/org/kitteh/irc/client/library/util/Mask.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,24 @@
*/
package org.kitteh.irc.client.library.util;

import org.kitteh.irc.client.library.element.Channel;
import org.kitteh.irc.client.library.element.User;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* Represents a mask that can match a {@link User}.
*/
public class Mask {
public class Mask implements Predicate<User> {
/**
* Creates a Mask from a given String.
*
Expand All @@ -39,13 +49,132 @@ public class Mask {
*/
@Nonnull
public static Mask fromString(@Nonnull String string) {
return new Mask(Sanity.nullCheck(string, "String cannot be null"));
Sanity.nullCheck(string, "String cannot be null");
return new Mask(string);
}

private final String string;
/**
* Creates a Mask from a given User.
*
* @param user user
* @return mask from user
*/
@Nonnull
public static Mask fromUser(@Nonnull User user) {
Sanity.nullCheck(user, "User cannot be null");
return new Mask(user.getHost(), user.getNick(), user.getUserString());
}

/**
* Creates a Mask from the given nick.
*
* @param nick nick
* @return mask from nick
*/
@Nonnull
public static Mask fromNick(@Nonnull String nick) {
Sanity.nullCheck(nick, "Nick cannot be null");
return new Mask(nick, null, null);
}

private Mask(@Nonnull String string) {
this.string = string;
/**
* Creates a Mask from the given user string.
*
* @param user user
* @return mask from user
*/
@Nonnull
public static Mask fromUserString(@Nonnull String user) {
Sanity.nullCheck(user, "User cannot be null");
return new Mask(null, user, null);
}

/**
* Creates a Mask from the given host.
*
* @param host host
* @return mask from host
*/
@Nonnull
public static Mask fromHost(@Nonnull String host) {
Sanity.nullCheck(host, "Host cannot be null");
return new Mask(null, null, host);
}

// Valid nick chars: \w\[]^`{}|-_
// Pattern unescaped: ([\w\\\[\]\^`\{\}\|\-_]+)!([~\w]+)@([\w\.\-:]+)
// You know what? Screw it.
// Let's just do it assuming no IRCD can handle following the rules.
// New pattern: ([^!@]+)!([^!@]+)@([^!@]+)
public static final Pattern NICK_PATTERN = Pattern.compile("([^!@]+)!([^!@]+)@([^!@]+)");
public static final char WILDCARD_CHARACTER = '*';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ?

public static final String WILDCARD = String.valueOf(WILDCARD_CHARACTER);
protected final Optional<String> nick;
protected final Optional<String> user;
protected final Optional<String> host;
protected final Pattern pattern;

protected Mask(@Nonnull String string) {
Sanity.nullCheck(string, "String cannot be null");

@Nullable String nick = null;
@Nullable String user = null;
@Nullable String host = null;

final Matcher matcher = NICK_PATTERN.matcher(string);
if (matcher.matches()) {
nick = matcher.group(1);
user = matcher.group(2);
host = matcher.group(3);
}

this.nick = Optional.ofNullable(nick);
this.user = Optional.ofNullable(user);
this.host = Optional.ofNullable(host);
this.pattern = StringUtil.wildcardToPattern(string);
}

protected Mask(@Nullable String nick, @Nullable String user, @Nullable String host) {
this.nick = Optional.ofNullable(nick);
this.user = Optional.ofNullable(user);
this.host = Optional.ofNullable(host);
this.pattern = this.resolvePattern();
}

@Nonnull
protected Pattern resolvePattern() {
String pattern = this.asString();
return StringUtil.wildcardToPattern(pattern);
}

/**
* Gets the nick component of this mask.
*
* @return nick component if known
*/
@Nonnull
public Optional<String> getNick() {
return this.nick;
}

/**
* Gets the user component of this mask.
*
* @return user component if known
*/
@Nonnull
public Optional<String> getUser() {
return this.user;
}

/**
* Gets the host component of this mask.
*
* @return host component if known
*/
@Nonnull
public Optional<String> getHost() {
return this.host;
}

/**
Expand All @@ -55,22 +184,62 @@ private Mask(@Nonnull String string) {
*/
@Nonnull
public String asString() {
return this.string;
return this.getNick().orElse(WILDCARD) + '!' + this.getUser().orElse(WILDCARD) + '@' + this.getHost().orElse(WILDCARD);
}

/**
* Gets a set of users that match this mask in the provided channel.
*
* @param channel channel
* @return set of users that match this mask
*/
@Nonnull
public Set<User> getMatches(@Nonnull Channel channel) {
Sanity.nullCheck(channel, "Channel cannot be null");
return channel.getUsers().stream().filter(this).collect(Collectors.toCollection(HashSet::new));
}

/**
* Gets if the user matches this mask.
*
* @param user user
* @return true if user matches this mask
*/
@Override
public boolean test(@Nonnull User user) {
Sanity.nullCheck(user, "User cannot be null");
return this.test(user.getName());
}

/**
* Gets if the string matches this mask.
*
* @param string string
* @return true if string matches this mask
*/
public boolean test(@Nonnull String string) {
Sanity.nullCheck(string, "String cannot be null");
return this.pattern.matcher(string).matches();
}

@Override
public int hashCode() {
return (2 * this.string.hashCode()) + 5;
return Objects.hash(this.getNick(), this.getUser(), this.getHost());
}

@Override
public boolean equals(Object o) {
return (o instanceof Mask) && ((Mask) o).string.equals(this.string);
if (!(o instanceof Mask)) {
return false;
}

final Mask that = (Mask) o;
return Objects.equals(this.getNick(), that.getNick()) && Objects.equals(this.getUser(), that.getUser()) && Objects.equals(this.getHost(), that.getHost());
}

@Nonnull
@Override
public String toString() {
return new ToStringer(this).add("string", this.string).toString();
return new ToStringer(this).add("nick", this.getNick()).add("user", this.getUser()).add("host", this.getHost()).add("pattern", this.pattern).toString();
}
}
49 changes: 49 additions & 0 deletions src/main/java/org/kitteh/irc/client/library/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.kitteh.irc.client.library.feature.CaseMapping;

import javax.annotation.Nonnull;
import java.util.regex.Pattern;

/**
* String tools!
Expand Down Expand Up @@ -158,4 +159,52 @@ public static String toLowerCase(@Nonnull Client client, @Nonnull String input)
return client.getServerInfo().getCaseMapping().toLowerCase(input);
}

/**
* Converts an IRC wildcard (*, ?) to a regular expression pattern.
*
* @param wildcardExpression expression
* @return a pattern
* @throws IllegalArgumentException if wildcardExpression is null
*/
@Nonnull
public static Pattern wildcardToPattern(@Nonnull String wildcardExpression) {
Sanity.nullCheck(wildcardExpression, "Wildcard expression cannot be null");

StringBuilder builder = new StringBuilder(wildcardExpression.length());
for (char character : wildcardExpression.toCharArray()) {
switch (character) {
case '*':
builder.append('.').append(character); // * to .*
break;
case '?':
builder.append('.'); // ? to .
break;
case '<':
case '(':
case '[':
case '{':
case '\\':
case '^':
case '-':
case '=':
case '$':
case '!':
case '|':
case ']':
case '}':
case ')':
case '+':
case '.':
case '>':
builder.append('\\');
// Deliberately fall through!
default:
builder.append(character);
break;
}
}
builder.insert(0, '^');
builder.append('$');
return Pattern.compile(builder.toString(), Pattern.CASE_INSENSITIVE);
}
}
Loading