*
* @var string
*/
- protected $signature = 'chatlib:database {which=de} {size=7}';
+ protected $signature = 'chatlib:database {which=de} {size=3}';
/**
* The console command description.
$query->whereNull('detected_language');
$query->orWhere('detected_language', '=', $lang);
})
- ->whereRaw('LENGTH(`text_content`) > 10')
+ ->orderBy('channel_id')
+ ->orderBy('created_at')
->chunk(5000, function ($msgs) use (&$count, $db) {
+ $previous = null;
foreach ($msgs as $msg) {
- $db->addMessage($msg);
+ $db->addMessage($msg, $previous);
+ $previous = $msg;
++$count;
}
$this->line($count);
*
* @var string
*/
- protected $signature = 'chatlib:generate {which=de} {amount=50}';
+ protected $signature = 'chatlib:generate {which=de} {amount=50} {context?}';
/**
* The console command description.
$amount = intval($this->argument('amount'));
for ($i = 0; $i < $amount; ++$i) {
- $this->line($db->generate());
+ $this->line($db->generate($this->argument('context')));
}
return 0;
ChatLog::whereIn('type', ['chat', 'error'])
->where('banned', false)
->orderBy('created_at')
- ->chunk(10000, function ($logs) use (&$good, &$bad) {
+ ->chunk(5000, function ($logs) use (&$good, &$bad) {
foreach ($logs as $line) {
try {
$line->evaluate();
- $line->evaluated_at = now();
- $line->save();
+ if ($line->isDirty()) {
+ $line->evaluated_at = now();
+ $line->save();
+ }
++$good;
} catch (\Exception $e) {
++$bad;
$end = Carbon::parse($entry['end']['dateTime'])->setTimezone('UTC');
$episode->estimate = $start->diffInSeconds($end);
$episode->confirmed = true;
- if (preg_match('/^(.*) - (.*?) vs\.? (.*?)$/u', $episode->title, $matches)) {
+ if (preg_match('/^(.*) - (.*?) vs\.? (.*?) vs\.? (.*?) vs\.? (.*?)$/u', $episode->title, $matches)) {
+ $episode->title = $matches[1];
+ $episode->save();
+ $this->syncPlayer($episode, $matches[2]);
+ $this->syncPlayer($episode, $matches[3]);
+ $this->syncPlayer($episode, $matches[4]);
+ $this->syncPlayer($episode, $matches[5]);
+ } else if (preg_match('/^(.*) - (.*?) vs\.? (.*?) vs\.? (.*?)$/u', $episode->title, $matches)) {
+ $episode->title = $matches[1];
+ $episode->save();
+ $this->syncPlayer($episode, $matches[2]);
+ $this->syncPlayer($episode, $matches[3]);
+ $this->syncPlayer($episode, $matches[4]);
+ } else if (preg_match('/^(.*) - (.*?) vs\.? (.*?)$/u', $episode->title, $matches)) {
$episode->title = $matches[1];
$episode->save();
$this->syncPlayer($episode, $matches[2]);
if (!$token) {
$token = new TwitchToken();
$token->nick = $this->argument('nick');
- $token->scope = ['chat:read', 'chat:edit'];
+ $token->scope = ['chat:read', 'chat:edit', 'whispers:read', 'user:manage:whispers'];
}
$url = $token->getAuthUrl();
$this->line('Please visit '.$url);
class ChatLib {
- public function __construct($size = 6) {
+ public function __construct($size = 3) {
$this->size = $size;
-
$converted = [];
foreach ($this->categories as $category => $patterns) {
$converted_patterns = [];
foreach ($patterns as $pattern) {
$converted_patterns[] = '/\b'.$pattern.'\b/u';
}
- $converted['%'.strtoupper($category).'%'] = $converted_patterns;
+ $converted[strtoupper($category)] = $converted_patterns;
}
$this->categories = $converted;
}
- public function addMessage(ChatLog $msg) {
- $this->addText($msg->text_content);
+ public function addMessage(ChatLog $msg, ChatLog $previous = null) {
+ if ($msg->isReply()) {
+ $this->addText($msg->text_content, $msg->getReplyParent());
+ } else if (!is_null($previous)) {
+ $this->addText($msg->text_content, $previous->text_content);
+ } else {
+ $this->addText($msg->text_content);
+ }
}
- public function addText($text) {
+ public function addText($text, $context = '') {
$tokens = $this->tokenize($text);
- if (empty($tokens)) return;
- $tokens[] = '';
- foreach ($tokens as $num => $token) {
- if ($num === 0) {
- $this->addTransition([], $token);
- } else {
- $start = max(0, $num - $this->size - 1);
- $end = $num;
- for ($i = $start; $i < $end; ++$i) {
- $this->addTransition(array_slice($tokens, $i, $end - $i), $token);
- if ($end - $i < 5) break;
- }
+ for ($i = 0; $i < count($tokens) - $this->size; ++$i) {
+ $this->addTransition(array_slice($tokens, $i, $this->size), $tokens[$i + $this->size]);
+ }
+ if (!empty($context)) {
+ $tokens = $this->tokenizeWithContext($text, $context);
+ $size = min($this->size - 1, count($tokens) - $this->size);
+ for ($i = 0; $i < $size; ++$i) {
+ $this->addTransition(array_slice($tokens, $i, $this->size), $tokens[$i + $this->size]);
}
- $this->addExample(array_slice($tokens, 0, $num), $token);
}
}
public function compile() {
foreach ($this->transitions as $key => $values) {
- $this->transitions[$key] = $this->index($values, 2);
- if (empty($this->transitions[$key])) {
- unset($this->transitions[$key]);
- }
- }
- foreach ($this->examples as $key => $values) {
- if (in_array($key, ['', ' '])) {
- unset($this->examples[$key]);
- continue;
- }
- $this->examples[$key] = $this->index($values, 1);
- if (empty($this->examples[$key]) || (count($this->examples[$key]) === 1 && $this->examples[$key][0][0] === $key)) {
- unset($this->examples[$key]);
- }
+ $this->transitions[$key] = $this->index($values);
}
}
- public function generate($limit = 100) {
- $tokens = [''];
- $generated = '';
- while (strlen($generated) < $limit) {
- $next = $this->randomNext($tokens);
- if ($next === '') break;
- $tokens[] = $next;
- $generated .= $next;
+ public function generate($context = null) {
+ if (!is_null($context)) {
+ $tokens = $this->tokenizeWithContext('', $context);
+ $generated = $this->loop($tokens);
+ if (!empty($generated)) {
+ return $generated;
+ }
}
- return $generated;
+ $tokens = $this->tokenize('');
+ return $this->loop($tokens);
}
public function saveAs($name) {
$data = [
'size' => $this->size,
'transitions' => $this->transitions,
- 'examples' => $this->examples,
];
Storage::disk('chatlib')->put($name.'.json', json_encode($data));
}
$data = json_decode(Storage::disk('chatlib')->get($name.'.json'), true);
$this->size = $data['size'];
$this->transitions = $data['transitions'];
- $this->examples = $data['examples'];
}
- private function index($arr, $min_weight = 2) {
+ private function index($arr) {
$result = [];
$sum = 0;
foreach ($arr as $key => $weight) {
- if ($weight < $min_weight) continue;
$lower = $sum;
$sum += $weight;
$result[] = [$key, $lower, $sum];
return $result;
}
- private function randomNext($tokens) {
- $cnt = count($tokens);
- for ($size = min($this->size, $cnt); $size > 0; --$size) {
- $cmb = $this->generalize(array_slice($tokens, -$size));
- if (isset($this->transitions[$cmb])) {
- $pick = $this->pick($this->transitions[$cmb]);
- if (!is_null($pick)) {
- return $this->exampleOf($pick, $tokens);
- }
- }
+ private function loop($tokens) {
+ while (count($tokens) < 50) {
+ $next = $this->randomNext($tokens);
+ if ($next === ' ') break;
+ $tokens[] = $next;
}
- return '';
+ return $this->untokenize($tokens);
+ }
+
+ private function randomNext($tokens) {
+ $key = $this->makeKey($tokens);
+ if (!isset($this->transitions[$key])) return ' ';
+ $pick = $this->pick($this->transitions[$key]);
+ return $pick[0];
}
private function pick($options) {
return $options[$min_index];
}
- private function addTransition($state, $next) {
- $ctx = $this->generalize($state);
- $cmb = $this->generalize([$next]);
- if (!isset($this->transitions[$ctx])) {
- $this->transitions[$ctx] = [];
+ private function addTransition($tokens, $next) {
+ $key = $this->makeKey($tokens);
+ if (!isset($this->transitions[$key])) {
+ $this->transitions[$key] = [];
}
- if (!isset($this->transitions[$ctx][$cmb])) {
- $this->transitions[$ctx][$cmb] = 1;
+ if (!isset($this->transitions[$key][$next])) {
+ $this->transitions[$key][$next] = 1;
} else {
- ++$this->transitions[$ctx][$cmb];
+ ++$this->transitions[$key][$next];
}
}
- private function addExample($context, $token) {
- $cmb = $this->generalize([$token]);
- if (!isset($this->examples[$cmb])) {
- $this->examples[$cmb] = [];
- }
- if (!isset($this->examples[$cmb][$token])) {
- $this->examples[$cmb][$token] = 1;
- } else {
- ++$this->examples[$cmb][$token];
+ private function splitText($text) {
+ if (trim($text) === '') return [];
+ return preg_split('/\s+/u', $text);
+ }
+
+ private function makeKey($tokens) {
+ $key = $this->joinText(array_slice($tokens, $this->size * -1));
+ $key = mb_strtolower($key);
+ $key = str_replace(['.', ',', ':', ';', '!', '?', '^', '+', '-', '"', "'", '(', ')', '[', ']'], '', $key);
+ $key = preg_replace('/\d+/u', '0', $key);
+ foreach ($this->categories as $category => $patterns) {
+ $key = preg_replace($patterns, $category, $key);
}
+ return $key;
}
- private function tokenize($str) {
- return array_values(array_filter(preg_split('/\b/u', $str), function($token) {
- if ($token === '') return false;
- if (preg_match('/cheer\d+/u', strtolower($token))) return false;
- return true;
- }));
+ private function joinText($tokens) {
+ return implode(' ', $tokens);
}
- private function generalize($tokens) {
- $str = '';
- foreach ($tokens as $token) {
- $replaced = preg_replace('/\d+/u', '0', $token);
- $replaced = preg_replace('/\s+/u', ' ', $replaced);
- $replaced = preg_replace('/(.)\1{2,}/u', '$1$1', $replaced);
- $replaced = strtolower($replaced);
- foreach ($this->aliases as $canonical => $variants) {
- if (in_array($replaced, $variants)) {
- $replaced = $canonical;
- break;
- }
- if ($replaced === $canonical) {
- break;
- }
- }
- $str .= $replaced;
- }
- foreach ($this->categories as $category => $patterns) {
- $str = preg_replace($patterns, $category, $str);
- }
- return $str;
+ private function untokenize($tokens) {
+ return $this->joinText(array_slice($tokens, $this->size));
}
- private function exampleOf($pick, $context) {
- if (!isset($this->examples[$pick[0]])) {
- return $pick[0];
+ private function tokenize($text) {
+ $tokens = $this->splitText($text);
+ $combined = array_merge(array_fill(0, $this->size, ' '), $tokens);
+ if (!empty($tokens)) {
+ $combined[] = ' ';
}
- if (isset($this->examples[$pick[0]])) {
- $example = $this->pick($this->examples[$pick[0]]);
- return $example[0];
+ return $combined;
+ }
+
+ private function tokenizeWithContext($text, $context) {
+ $combined = $this->tokenize($text);
+ $context_tokens = array_slice($this->splitText($context), $this->size * -1 + 1);
+ for ($i = 0; $i < count($context_tokens); ++$i) {
+ $combined[$this->size - $i - 2] = $context_tokens[count($context_tokens) - $i - 1];
}
- return $pick[0];
+ return $combined;
}
private $size;
private $transitions = [];
- private $examples = [];
-
- private $aliases = [
- 'chest' => ['kiste'],
- 'einen' => ['n', 'nen'],
- 'musik' => ['mukke'],
- 'schade' => ['schad', 'schaade'],
- ];
private $categories = [
'fail' => [
return TokenizedMessage::fromLog($this);
}
+ public function isReply() {
+ return !empty($this->tags['reply-parent-msg-body']);
+ }
+
+ public function getReplyParent() {
+ return str_replace('\\s', ' ', $this->tags['reply-parent-msg-body']);
+ }
+
+ public function getReplyParentUser() {
+ return $this->tags['reply-parent-display-name'];
+ }
+
+ public function getText() {
+ return $this->params[1];
+ }
+
public function getTextWithoutEmotes() {
- $text = $this->text_content;
+ $text = $this->params[1];
if (isset($this->tags['emotes']) && !empty($this->tags['emotes'])) {
$emotes = explode('/', $this->tags['emotes']);
foreach ($emotes as $emote) {
return trim(preg_replace('/\s+/', ' ', $text));
}
+ public function getTextWithoutReply() {
+ if ($this->isReply()) {
+ return mb_substr($this->params[1], mb_strlen($this->getReplyParentUser()) + 2);
+ }
+ return $this->params[1];
+ }
+
public function evaluate() {
$this->evaluateUser();
$this->evaluateChannel();
return;
}
- if ($this->command == 'PRIVMSG') {
+ if ($this->command == 'PRIVMSG' || $this->command == 'WHISPER') {
if (static::isKnownBot($this->nick)) {
$this->type = 'bot';
} else if (substr($this->params[0], 0, 1) == '#') {
} else {
$this->type = 'dm';
}
- $this->text_content = $this->params[1];
+ $this->text_content = $this->getTextWithoutReply();
$this->detectLanguage();
$tokenized = $this->tokenize();
if ($tokenized->isSpammy()) {
return TokenizedMessage::fromIRC($this);
}
+ public static function capReq($cap) {
+ $msg = new IRCMessage();
+ $msg->command = 'CAP REQ';
+ $msg->params[] = $cap;
+ return $msg;
+ }
+
public static function join($channels) {
$msg = new IRCMessage();
$msg->command = 'JOIN';
return $this->command == 'PRIVMSG';
}
+ public function isWhisper() {
+ return $this->command == 'WHISPER';
+ }
+
public function isOwner() {
return substr($this->getPrivMsgTarget(), 1) == $this->nick;
}
public function __construct($text, $tags = []) {
$this->text = trim($text);
$this->tags = $tags;
+ if (isset($tags['reply-parent-display-name'])) {
+ $this->text = mb_substr($text, mb_strlen($tags['reply-parent-display-name']) + 2);
+ }
$this->raw = strtolower(preg_replace('/[^\w]/u', '', $this->text));
$this->tokens = array_values(array_map('trim', array_filter(preg_split('/\b/u', strtolower($this->text)))));
- $this->emoteless = $this->text;
+ $this->emoteless = $text;
if (isset($this->tags['emotes']) && !empty($this->tags['emotes'])) {
$emotes = explode('/', $this->tags['emotes']);
foreach ($emotes as $emote) {
}
+ public function getText() {
+ return $this->text;
+ }
+
+
public function contains($text) {
return Str::contains($this->text, $text);
}
if ($this->contains(['€', '$', '@', '://'])) {
return true;
}
- if ($this->containsRaw(['followers', 'promotion', 'viewers'])) {
+ if ($this->containsRaw(['follow', 'promotion', 'viewer'])) {
return true;
}
if ($this->containsRaw('horsti')) {
$this->classification = 'cmd';
} else if ($this->isShort() && ($this->hasTokenThatStartsOrEndsWith(['gg']) || $this->hasEmoteThatEndsWith(['gg']))) {
$this->classification = 'gg';
- } else if ($this->isShort() && $this->containsRaw(['glgl', 'glhf', 'goodluck', 'hfgl', 'vielglück'])) {
+ } else if ($this->isShort() && ($this->containsRaw(['glgl', 'glhf', 'goodluck', 'hfgl', 'vielglück']) || $this->hasToken('gl'))) {
$this->classification = 'gl';
} else if ($this->hasToken(['danke', 'thanks', 'thx', 'ty']) && !$this->hasToken(['nah', 'nee', 'nein', 'no'])) {
$this->classification = 'thx';
$this->hasConsecutiveTokens(['how', 'much']) ||
$this->hasConsecutiveTokens(['wie', 'viele'])
) {
- return ['yes', 'no', 'kappa', 'lol', 'wtf', 'number'];
+ return ['yes', 'no', 'kappa', 'wtf', 'number'];
}
- return ['yes', 'no', 'kappa', 'lol', 'wtf'];
+ return ['yes', 'no', 'kappa', 'wtf'];
case 'rage':
return ['kappa', 'lol', 'rage'];
case 'wtf':
$this->handlePrivMsg($msg);
return;
}
+ if ($msg->isWhisper()) {
+ $this->handleWhisper($msg);
+ return;
+ }
if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
$this->logger->notice('login failed, refreshing access token');
$this->token->refresh();
$this->ready = true;
return;
}
+ if ($msg->command == 'GLOBALUSERSTATE') {
+ // receive own user metadata
+ $this->handleUserState($msg);
+ return;
+ }
}
public function getMessageChannel(IRCMessage $msg) {
public function handlePrivMsg(IRCMessage $msg) {
}
+ public function handleUserState(IRCMessage $msg) {
+ if (isset($msg->tags['user-id'])) {
+ $this->user_id = $msg->tags['user-id'];
+ }
+ }
+
+ public function handleWhisper(IRCMessage $msg) {
+ }
+
public function login() {
$this->ws->send('PASS oauth:'.$this->token->access);
$this->ws->send('NICK '.$this->nick);
$this->last_contact = time();
}
+ public function sendWhisper($to, $msg) {
+ $this->logger->info('sending whisper to '.$to.': '.$msg);
+ try {
+ $response = $this->token->request()->post('/whispers?from_user_id='.$this->user_id.'&to_user_id='.$to, [
+ 'message' => $msg,
+ ]);
+ if (!$response->successful()) {
+ $this->logger->error('sending whisper to '.$to.': '.$response->status());
+ }
+ } catch (\Exception $e) {
+ $this->logger->error('sending whisper to '.$to.': '.$e->getMessage());
+ }
+ }
+
protected function listenCommands() {
$this->getLoop()->addPeriodicTimer(1, function () {
private $nick;
private $token;
+ private $user_id = '';
private $connector;
private $ws;
$this->tagChannelRead($channel, $msg);
}
+ public function handleWhisper(IRCMessage $msg) {
+ $text = $this->chatlib->generate($msg->getText());
+ $this->sendWhisper($msg->tags['user-id'], $text);
+ }
+
public function getChatlibDatabase(Channel $channel) {
return $this->chatlib;
}
return;
}
$text = $this->contextualMsg($channel);
- if ($this->shouldAdlib($channel)) {
+ if (!$text && $this->shouldAdlib($channel)) {
$this->performAdlib($channel);
return;
}
private function performAdlib(Channel $channel) {
$db = $this->getChatlibDatabase($channel);
- $text = $db->generate();
+ $latest_msg = $this->getLatestMessage($channel);
+ $text = $db->generate($latest_msg->getText());
$this->tagChannelWrite($channel);
$this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
$log = new ChatBotLog();
$tokenized = $msg->tokenize();
if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
- $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
- if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
- array_shift($this->notes[$channel->id]['latest_msgs']);
- }
+ $this->noteChannelMessage($channel, $tokenized);
}
if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
+ $this->noteChannelMessage($channel, $tokenized);
$this->notes[$channel->id]['wait_msgs'] = 0;
$this->notes[$channel->id]['wait_time'] = 0;
$response = $tokenized->getResponseCategory();
}
}
+ private function noteChannelMessage(Channel $channel, TokenizedMessage $tokenized) {
+ $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
+ if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
+ array_shift($this->notes[$channel->id]['latest_msgs']);
+ }
+ }
+
private function tagChannelWrite(Channel $channel) {
$this->getNotes($channel);
$this->notes[$channel->id]['last_write'] = time();
}
private function getLatestMessage(Channel $channel) {
- $this->getNotes($channel);
+ $notes = $this->getNotes($channel);
if (!empty($notes['latest_msgs'])) {
return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
}