@@ -1,11 +1,14 @@
package com.ryonday.automation.twitch.domain;

import com.google.common.base.MoreObjects;

import javax.persistence.*;
import java.util.Objects;

@Entity
@Table(name = "emotes_in_chat",
uniqueConstraints = {@UniqueConstraint(columnNames = {"chat_id", "startIndex"}),
@UniqueConstraint(columnNames = {"chat_id", "endIndex"})})
uniqueConstraints = {@UniqueConstraint(columnNames = {"chat_id", "startIndex"}),
@UniqueConstraint(columnNames = {"chat_id", "endIndex"})})
public class EmoteInChat {

@Id
@@ -17,18 +20,18 @@ public class EmoteInChat {
private Long version;

@ManyToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.EAGER)
@JoinColumn(name = "chat_id", nullable = false, updatable = false)
@JoinColumn(name = "chat_id", referencedColumnName = "id", nullable = false, updatable = false)
private TwitchChatMessage chat;

@ManyToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.EAGER)
@JoinColumn(name = "emote_id")
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.DETACH}, optional = false, fetch = FetchType.EAGER)
@JoinColumn(name = "emote_id", referencedColumnName = "id", nullable = false, updatable = false)
private Emote emote;

@Column(name = "startIndex", nullable = false)
private Long startIndex;
@Column(name = "startIndex", nullable = false, updatable = false)
private Integer startIndex;

@Column(name = "endIndex", nullable = false)
private Long endIndex;
@Column(name = "endIndex", nullable = false, updatable = false)
private Integer endIndex;

public Long getId() {
return id;
@@ -52,31 +55,66 @@ public TwitchChatMessage getChat() {
return chat;
}

public EmoteInChat setChat(TwitchChatMessage chat) {
if (chat != null) {
this.setChat(chat);
}
return this;
}

public Emote getEmote() {
return emote;
}

public EmoteInChat setEmote(Emote emote) {
this.emote = emote;
emote.addUse(this);
return this;
}

public Long getEndIndex() {
public Integer getEndIndex() {
return endIndex;
}

public EmoteInChat setEndIndex(Long endIndex) {
public EmoteInChat setEndIndex(Integer endIndex) {
this.endIndex = endIndex;
return this;
}


public Long getStartIndex() {
public Integer getStartIndex() {
return startIndex;
}

public EmoteInChat setStartIndex(Long startIndex) {
public EmoteInChat setStartIndex(Integer startIndex) {
this.startIndex = startIndex;
return this;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof EmoteInChat)) return false;
EmoteInChat that = (EmoteInChat) o;
return Objects.equals(emote, that.emote) &&
Objects.equals(startIndex, that.startIndex) &&
Objects.equals(endIndex, that.endIndex);
}

@Override
public int hashCode() {
return Objects.hash(emote, startIndex, endIndex);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(EmoteInChat.class)
.add("id", id)
.add("version", version)
.add("chat", chat)
.add("emote", emote)
.add("startIndex", startIndex)
.add("endIndex", endIndex)
.toString();
}
}
@@ -1,8 +1,12 @@
package com.ryonday.automation.twitch.domain;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;

import javax.persistence.*;
import java.util.List;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "nicknames",
@@ -17,11 +21,11 @@ public class Nickname implements Comparable<Nickname> {
@Column(name = "version", columnDefinition = "integer DEFAULT 0", nullable = false)
private Long version;

@Column(name = "nickname", nullable = false)
@Column(name = "nickname", nullable = false, updatable = false)
private String nickname;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "nickname", fetch = FetchType.LAZY)
private List<TwitchChatMessage> chats;
private Set<TwitchChatMessage> chats = Sets.newHashSet();

public Long getId() {
return id;
@@ -50,8 +54,23 @@ public Nickname setNickname(String nickname) {
return this;
}

public List<TwitchChatMessage> getChats() {
return chats;
public Set<TwitchChatMessage> getChats() {
return ImmutableSortedSet.copyOf(chats);
}

public Nickname addChats( Set<TwitchChatMessage> chats ) {
if( chats != null ) {
chats.forEach( this::addChat);
}
return this;
}

public Nickname addChat( TwitchChatMessage chat ) {
if( !chats.contains( chat )) {
chats.add( chat );
chat.setNickname( this );
}
return this;
}

@Override
@@ -67,6 +86,15 @@ public int hashCode() {
return Objects.hash(nickname);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(Nickname.class)
.add("id", id)
.add("version", version)
.add("nickname", nickname)
.toString();
}

@Override
public int compareTo(Nickname o) {
return this.nickname.compareTo(o.nickname);
@@ -1,12 +1,17 @@
package com.ryonday.automation.twitch.domain;

import com.beust.jcommander.internal.Sets;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSortedSet;

import javax.persistence.*;
import java.util.List;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "twitch_channels", uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})})
public class TwitchChannel implements Comparable<TwitchChannel>{
public class TwitchChannel implements Comparable<TwitchChannel>{

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@@ -19,8 +24,8 @@ public class TwitchChannel implements Comparable<TwitchChannel>{
@Column(name = "name", nullable = false)
private String name;

@OneToMany(cascade = CascadeType.ALL, mappedBy = "name", fetch = FetchType.LAZY)
private List<TwitchChatMessage> chats;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "channel", fetch = FetchType.LAZY)
private Set<TwitchChatMessage> chats = Sets.newHashSet();

public Long getId() {
return id;
@@ -49,8 +54,23 @@ public TwitchChannel setName(String name) {
return this;
}

public List<TwitchChatMessage> getChats() {
return chats;
public Set<TwitchChatMessage> getChats() {
return ImmutableSortedSet.copyOf(chats);
}

public TwitchChannel addChats( Collection<TwitchChatMessage> chats) {
if( chats != null ) {
chats.forEach(this::addChat);
}
return this;
}

public TwitchChannel addChat( TwitchChatMessage chat ) {
if( chat != null && !chats.contains( chat )) {
chats.add( chat );
chat.setChannel( this );
}
return this;
}

@Override
@@ -66,6 +86,15 @@ public int hashCode() {
return Objects.hash(name);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(TwitchChannel.class)
.add("id", id)
.add("version", version)
.add("name", name)
.toString();
}

@Override
public int compareTo(TwitchChannel o) {
return this.name.compareTo( o.name);
@@ -1,11 +1,16 @@
package com.ryonday.automation.twitch.domain;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;

import javax.persistence.*;
import java.awt.Color;
import java.awt.*;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "twitch_chat_messages")
@@ -19,24 +24,24 @@ public class TwitchChatMessage implements Comparable<TwitchChatMessage> {
@Column(name = "version", columnDefinition = "integer DEFAULT 0", nullable = false)
private Long version;

@Column(name = "timestamp", nullable = false)
@Column(name = "timestamp", nullable = false, updatable = false)
private LocalDateTime timestamp;

@Column(name = "nickColor", nullable = false)
@Column(name = "nickColor", nullable = false, updatable = false)
private Color nickColor;

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "nickname_id", nullable = false)
@ManyToOne(optional = false, cascade = {CascadeType.MERGE, CascadeType.DETACH}, fetch = FetchType.EAGER)
@JoinColumn(name = "nickname_id", referencedColumnName = "id", nullable = false, updatable = false)
private Nickname nickname;

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "channel_id", nullable = false)
@ManyToOne(optional = false, cascade = {CascadeType.MERGE, CascadeType.DETACH}, fetch = FetchType.EAGER)
@JoinColumn(name = "channel_id", referencedColumnName = "id", nullable = false, updatable = false)
private TwitchChannel channel;

@OneToMany(mappedBy = "chat", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<EmoteInChat> chatEmotes;
private Set<EmoteInChat> chatEmotes = Sets.newHashSet();

@Column(name = "text", nullable = false)
@Column(name = "text", nullable = false, updatable = false)
private String text;

public Long getId() {
@@ -72,6 +77,17 @@ public TwitchChannel getChannel() {

public TwitchChatMessage setChannel(TwitchChannel channel) {
this.channel = channel;
channel.addChat(this);
return this;
}

public Nickname getNickname() {
return nickname;
}

public TwitchChatMessage setNickname(Nickname nickname) {
this.nickname = nickname;
nickname.addChat(this);
return this;
}

@@ -84,12 +100,22 @@ public TwitchChatMessage setNickColor(Color nickColor) {
return this;
}

public Nickname getNickname() {
return nickname;
public Set<EmoteInChat> getChatEmotes() {
return ImmutableSortedSet.copyOf(chatEmotes);
}

public TwitchChatMessage setNickname(Nickname nickname) {
this.nickname = nickname;
public TwitchChatMessage addChatEmotes(Collection<EmoteInChat> chatEmotes) {
if (chatEmotes != null) {
chatEmotes.forEach(this::addChatEmote);
}
return this;
}

public TwitchChatMessage addChatEmote(EmoteInChat emoteInChat) {
if (emoteInChat != null && !chatEmotes.contains(emoteInChat)) {
chatEmotes.add(emoteInChat);
emoteInChat.setChat(this);
}
return this;
}

@@ -107,24 +133,37 @@ public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TwitchChatMessage)) return false;
TwitchChatMessage chat = (TwitchChatMessage) o;
return Objects.equals(id, chat.id) &&
Objects.equals(timestamp, chat.timestamp) &&
Objects.equals(nickColor, chat.nickColor) &&
Objects.equals(nickname, chat.nickname) &&
Objects.equals(channel, chat.channel) &&
Objects.equals(text, chat.text);
return Objects.equals(timestamp, chat.timestamp) &&
Objects.equals(nickColor, chat.nickColor) &&
Objects.equals(nickname, chat.nickname) &&
Objects.equals(channel, chat.channel) &&
Objects.equals(text, chat.text);
}

@Override
public int hashCode() {
return Objects.hash(id, timestamp, nickColor, nickname, channel, text);
return Objects.hash(timestamp, nickColor, nickname, channel, text);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(TwitchChatMessage.class)
.add("channel", channel)
.add("id", id)
.add("version", version)
.add("timestamp", timestamp)
.add("nickColor", nickColor)
.add("nickname", nickname)
.add("chatEmotes", chatEmotes)
.add("text", text)
.toString();
}

@Override
public int compareTo(TwitchChatMessage o) {
return channelAndTimestamp.compare( this, o );
return channelAndTimestamp.compare(this, o);
}

public final static Comparator<TwitchChatMessage> channelAndTimestamp = Comparator.comparing(TwitchChatMessage::getChannel).thenComparing(TwitchChatMessage::getTimestamp);
public final static Comparator<TwitchChatMessage> nicknameAndTimestamp = Comparator.comparing( TwitchChatMessage::getNickname ).thenComparing( TwitchChatMessage::getTimestamp );
public final static Comparator<TwitchChatMessage> nicknameAndTimestamp = Comparator.comparing(TwitchChatMessage::getNickname).thenComparing(TwitchChatMessage::getTimestamp);
}
@@ -1,22 +1,20 @@
package com.ryonday.automation.twitch.handler;

import com.google.common.base.Preconditions;
import com.ryonday.automation.twitch.EmoteTag;
import com.ryonday.automation.twitch.domain.Nickname;
import com.ryonday.automation.twitch.domain.TwitchChannel;
import com.ryonday.automation.twitch.domain.TwitchChatMessage;
import com.ryonday.automation.twitch.repo.*;
import com.ryonday.automation.twitch.util.EmoteTagParser;
import org.pircbotx.hooks.ListenerAdapter;
import org.pircbotx.hooks.events.MessageEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.awt.*;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.Set;

@Component
public class TwitchMessageSQLHandler extends ListenerAdapter {
@@ -29,6 +27,7 @@ public class TwitchMessageSQLHandler extends ListenerAdapter {
private final ChatEmoteRepository chatEmoteRepo;
private final TwitchChannelRepository chanRepo;

@Autowired
public TwitchMessageSQLHandler(TwitchChannelRepository chanRepo,
NicknameRepository nickRepo,
TwitchChatMessageRepository chatRepo,
@@ -42,48 +41,65 @@ public TwitchMessageSQLHandler(TwitchChannelRepository chanRepo,
}

@Override
@Transactional
public void onMessage(MessageEvent messageEvent) throws Exception {
logger.debug("Received Channel Message: {}", messageEvent);
String chanName = messageEvent.getChannel().getName();
Long twitchUserId = Long.parseLong(messageEvent.getTags().get("user-id"));
String emoteString = messageEvent.getTags().get("emotes");
String nickColor = messageEvent.getTags().get("color");
String text = messageEvent.getMessage();
final String chanName = messageEvent.getChannel().getName();
final Long twitchUserId = Long.parseLong(messageEvent.getTags().get("user-id"));
final String emoteString = messageEvent.getTags().get("emotes");
final String nickColor = messageEvent.getTags().get("color");
final String text = messageEvent.getMessage();

Optional<TwitchChannel> maybeChannel = chanRepo.findByName(chanName);
Optional<Nickname> maybeNick = nickRepo.findOne(twitchUserId);
try {

TwitchChannel channel = chanRepo
.findByName(chanName)
.orElse(
chanRepo.save(
new TwitchChannel()
.setName(chanName)
));
TwitchChannel channel = chanRepo
.findByNameIgnoreCase(chanName)
.orElseGet(
() -> chanRepo.save(
new TwitchChannel()
.setName(chanName))
);

Nickname nickname = nickRepo
.findOne(twitchUserId)
.orElse(
nickRepo.save(
new Nickname()
.setId( twitchUserId )
.setNickname(messageEvent.getTags().get("display-name").toLowerCase())
));
logger.info("channel: {}", channel );
Nickname nickname = nickRepo
.findOne(twitchUserId)
.orElseGet(
() -> nickRepo.save(
new Nickname()
.setId(twitchUserId)
.setNickname(messageEvent.getTags().get("display-name").toLowerCase()))
);

Set<EmoteTag> emoteTags = EmoteTagParser.parseEmoteTag( emoteString );
logger.info("nickname: {}", nickname );
// Set<EmoteInChat> emotesInChat = EmoteTagParser
// .parseEmoteTag(emoteString)
// .stream()
// .map(tag -> new EmoteInChat()
// .setStartIndex(tag.startIndex)
// .setEndIndex(tag.endIndex)
// .setEmote(emoteRepo
// .findOne(tag.id)
// .orElseGet(() ->
// emoteRepo.save(
// new Emote()
// .setId(tag.id)
// .setEmote(
// text.substring(
// tag.startIndex,
// tag.endIndex))))))
// .collect(Collectors.toSet());

TwitchChatMessage message = new TwitchChatMessage()
.setChannel( channel )
.setText( text )
.setNickname( nickname )
.setNickColor(Color.decode(nickColor.substring(1)))
.setTimestamp( LocalDateTime.now() );

TwitchC
chatRepo.save(
new TwitchChatMessage()
.setChannel(channel)
.setNickname(nickname)
.setText(text)
.setTimestamp(LocalDateTime.now())
.setNickColor(Color.decode(nickColor)));

if (messageEvent.getMessage().startsWith("HoomanBot")) {
messageEvent.respond("HELLO HOOMAN");
} catch (Exception ex) {
logger.error("Received exception.", ex);
}

}
}
@@ -6,5 +6,5 @@

public interface TwitchChannelRepository extends TwitchRepository<TwitchChannel, Long> {

Optional<TwitchChannel> findByName(String name);
Optional<TwitchChannel> findByNameIgnoreCase(String name);
}
@@ -42,10 +42,10 @@ public class EmoteTagParser {
private final static Splitter RANGE = Splitter.on('-').limit(2).trimResults();

public static Set<EmoteTag> parseEmoteTag(String emoteTag) {
if (Strings.isNullOrEmpty(emoteTag) || !EMOTE_TAG_PATTERN.matcher(emoteTag).matches()) {
logger.warn("Emote tag content did not match expected emote tag pattern.\n\t" +
if (Strings.isNullOrEmpty(emoteTag) || !EMOTE_TAG_PATTERN.matcher(emoteTag).matches()) {
logger.warn("Empty/Null/noncompliant emote tag.\n\t" +
"Pattern: '{}'\n\t" +
"Tag: '{}'", emoteTag, EMOTE_TAG_PATTERN.toString());
"Tag: '{}'", EMOTE_TAG_PATTERN, emoteTag);
return ImmutableSet.of();
}
// ex: 100:10-20,21-31/200:41-51,52-62/400:70-80
@@ -17,4 +17,7 @@ twitch:
autoJoin: "#ryonday"
emoteUrl: http://static-cdn.jtvnw.net/emoticons/v1/%s/%s


logging:
level:
# org.hibernate: DEBUG
org.springframework.data: TRACE
@@ -0,0 +1,62 @@
package com.ryonday.automation.twitch.converter;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;

import static junit.framework.Assert.assertEquals;

public class ColorConverterTest {

Logger logger = LoggerFactory.getLogger( ColorConverterTest.class);

ColorConverter cc = new ColorConverter();

@Test
public void testWHISKYTANGFOXTROT() throws Exception {

String color = "#9ACD32";
Color c = Color.decode( color );
logger.info( "Color: {}", c );


}

@Test
public void testConvertToString() throws Exception {
Color c = new Color(255, 0, 0 );
String cs = cc.convertToDatabaseColumn( c );
assertEquals( "FFFF0000", cs );

c = new Color( 0, 255, 0 );
cs = cc.convertToDatabaseColumn( c );
assertEquals( "FF00FF00", cs );

c = new Color( 0, 1, 255 );
cs = cc.convertToDatabaseColumn( c );
assertEquals( "FF0001FF", cs );
}

@Test
public void testConvertToColor() throws Exception {
String cs = "FFFF0000";
Color c = cc.convertToEntityAttribute( cs );
assertEquals( 255, c.getRed() );
assertEquals( 0, c.getGreen() );
assertEquals( 0, c.getBlue() );

cs = "FF00FF00";
c = cc.convertToEntityAttribute( cs );
assertEquals( 0, c.getRed() );
assertEquals( 255, c.getGreen() );
assertEquals( 0, c.getBlue() );

cs = "FF0101FF";
c = cc.convertToEntityAttribute( cs );
assertEquals( 1, c.getRed() );
assertEquals( 1, c.getGreen() );
assertEquals( 255, c.getBlue() );
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml" />
<logger name="org.springframework.web" level="DEBUG" />
<logger name="com.ryonday" level="DEBUG" />
</configuration>