## Text-To-Speech script for ircII EPIC5
##
## This script can either run a local program external
## to EPIC5 that takes input on STDIN to produce speech
## output, or it can connect to a defined IP and port via
## TCP and send the same data to that daemon instead, making
## it possible to run a daemon on a Windows system where a
## greater variety of voices may be available.
##
## The external program or TCP daemon are handed the arguments
## of $* from /speak if SPEECH is set to be ON. /speak is called
## by /cspeak, /mspeak and /nspeak which are wrappers to simplify
## things for us.
##

# This script takes advantage of "newaway" and "netsplit" if loaded
#load newaway
#load netsplit

package irctts

## Set some defaults and bind some keys.

addset speech bool
addset speech_speak_all_channels bool
addset speech_speak_modes int
addset speech_output str
addset speech_rc_file str
addset speech_hear_myself bool
addset speech_default_voice str
addset speech_default_msg_voice str
addset speech_default_notice_voice str
addset speech_default_self_voice str
addset speech_queue bool

set speech on
set speech_default_voice ms_mike:9
set speech_default_notice_voice rs_jill:5
set speech_default_msg_voice rs_daniel:5
set speech_default_self_voice loq_dave:2
set speech_output sock:persephone 8255
set speech_rc_file ~/.ircttsrc
set mode_stripper on

bind ^S parse_command set speech toggle
bind ^D parse_command tts.addsrc $T
bind ^F parse_command {set speech_queue toggle;^queue -do tts}

## Set up events to speak

on ^exec "irctts *"
on ^dcc_raw '$_tts.sockfd *'

on #-msg 5 "*" {mspeak $0 $1-}
on #-dcc_chat 5 "*" {mspeak $0 $1-}
on #-action 5 '% $N *' {mspeak $0 $ts($0) $2-}
on #-notice 5 "*" {mspeak $0 $ts($0) NOTICE: $1-}

on #-public 5 "*" {cspeak $1 $0 $2-}
on #-public_other 5 "*" {cspeak $1 $0 $2-}
on #-action 5 '*' {cspeak $1 $0 $ts($0) $2-}

on #-join -5 "*" {
	@:so1 = encode($tolower($1))
	@:so2 = encode($0)
	if (signoffs[$so1][$so2]) {
		if (isbroke[$signoffs[$so1][$so2]]) {
			nspeak force Netsplit finished.
		}
	} else {
		nspeak $1 $ts($0) has joined channel $ts($1)
	}
}
on #-channel_signoff 5 "*" {
	@:cs1 = encode($tolower($0))
	@:cs2 = encode($1)
	@:cs3 = encode($2).$encode($3)

	if (isbroke[$cs3] ) {[
		nspeak $0 $ts($1) has quit IRC: $2-
	} else {
		nspeak force Netsplit detected
	}
}
on #-part 5 "*" {nspeak $1 $ts($0) has left channel $ts($1)}
on #-channel_nick 5 "*" {nspeak $0 $ts($1) is now known as $ts($2)}
on #-topic 5 "*" {nspeak $1 $ts($0) set new topic on $ts($1): $2-}
on #-kick 5 "*" {nspeak $2 $ts($1) kicked $ts($0) from $ts($2): $3-}
on #-kick 5 '$N *' {nspeak force $ts($1) kicked you from $ts($2): $3-}
on #-mode_stripped 5 "*" { if (ischannel($1)) {modespeak $*} }

on #-send_public 5 "*" {mespeak $0 $1-}
on #-send_msg 5 "*" {mespeak $0 $1-}
on #-send_action 5 "*" {mespeak $0 $ts($N) $1-}
on #-send_dcc_chat 5 "*" {mespeak $0 $1-}

on #-notify_signon 5 "*" {nspeak force $ts($0) has just signed on to I R C.}
on #-notify_signoff 5 "*" {nspeak force $ts($0) has signed off from I R C.}
on #-ctcp_reply 5 "*" {nspeak force CTCP $2 reply from $ts($0): $3-}

on #-301 5 "*" {
	# The or part is to force a true if newaway isn't loaded.
	if (newaway.show($*)||newaway.show($*)==[]) {
		nspeak force $0 is away: $1-
	}
}
on #-305 5 "*" {nspeak force $1-}
on #-306 5 "*" {nspeak force $1-}
on #-311 5 "*" {nspeak force $1 is $2 at $3. $5-}
on #-312 5 "*" {nspeak force $1 is on server $2}
on #-313 5 "*" {nspeak force $1-}
on #-317 5 "*" {nspeak force $1 has been idle $tdiff($2)}
on #-319 5 "*" {nspeak force $1 is on channels $2-}
on #-401 5 "*" {nspeak force Error. $1: $2-}
on #-402 5 "*" {nspeak force Error. $1: $2-}
on #-403 5 "*" {nspeak force Error. $1: $2-}
on #-404 5 "*" {nspeak force Error. $1: $2-}
on #-405 5 "*" {nspeak force Error. $1: $2-}
on #-406 5 "*" {nspeak force Error. $1: $2-}

on #-invite 5 "*" {
	nspeak force $ts($0) invited you to $ts($1).
	nspeak force Type /join minus i to join this channel.
}

on #-dcc_request 5 "*" {dccspeak $*}
on #-dcc_offer 5 "*" {dccspeak $0 OFFR $1-}
on #-dcc_connect 5 "*" {dccspeak $0 INIT $1-}
on #-dcc_lost 5 "*" {dccspeak $0 LOST $1-}

on #-server_lost 5 "*" {nspeak force Disconnected from $1: $2-}
on #-server_state 5 "% % CONNECTING" {
	nspeak force Connecting to server $serverctl(GET $0 name)
}
on #-server_state 5 "% % ACTIVE" {
	nspeak force Connected to server $serverctl(GET $0 name)
}

## Setup our commands

alias speak {
	if (getset(speech)==[ON] && _tts.last != [$*]) {
		defer
		@_tts.last = [$*]
		^queue tts {speakit $0 $tts.patsub($1-)}
		if (getset(speech_queue)==[OFF]) {^queue -do tts}
		#speakit $0 $tts.patsub($1-)
	}
}

alias speakit {
	@:_tts.sot = before(: $getset(speech_output))
	@:_tts.soa = after(: $getset(speech_output))
	switch ( $_tts.sot ) {
		( sock ) {
			if (dccctl(TYPEMATCH RAW) == []) {
				@_tts.sockfd = connect($_tts.soa)
				pause 1
			}
			^msg =$_tts.sockfd $*
		}
		( pipe ) {
			# This will silently fail if %irctts already exists
			^exec -name irctts $_tts.soa
			^msg %irctts $*
		}
		( file ) {
			if (isfilevalid($_tts.sofd)||(_tts.sofd=open("$_tts.soa" w)) > -1) {
				@write($_tts.sofd $*)
			} else {
				xecho -b Error: Couldn't open file $soa
			}
		}
		( * ) { xecho -b Error: Unknown output type. }
	}
}

alias wtf {speakit $_tts.last}

alias dccspeak {
	switch ( $1 ) {
		( SEND ) {nspeak force $ts($0) wants to send you $remw($~ $5-), $~ bytes.}
		( CHAT ) {nspeak force $ts($0) wants to dcc chat with you}
		( INIT ) {nspeak force dcc $2 connection established with $ts($0)}
		( OFFR ) {
			if ([$2]==[SEND]) {
				nspeak force Sent dcc $2 request for $3, $4 bytes, to $ts($0)
			} else {
				nspeak force Sent dcc $2 request to $ts($0)
			}
		}
		( LOST ) {
			switch ( $2 ) {
				( GET )  {nspeak force dcc $2 of $3 from $ts($0) lost: $5-}
				( SEND ) {nspeak force dcc $2 to $ts($0) lost: $3-}
				( CHAT ) {nspeak force dcc $2 to $ts($0) lost: $3-}
				( RAW ) {
					# If we're speaking via tcp sock, trying to
					# speak now might be a bad idea.
					if (before(: $getset(speech_output))!=[sock]) {
						nspeak force dcc $2 to $ts($0) lost: $3-
					}
				}
				( * )    {nspeak force Unknown dcc $2 with $ts($0) lost: $3-}
			}
		}
		( * )    {nspeak force Unkonwn dcc request $1 from $ts($0). $2-}
	}
}

alias modespeak {
	switch ( $getset(speech_speak_modes) ) {
		( 1 ) { modespeaker obmsp $* }
		( 2 ) { modespeaker obmspv $* }
		( 3 ) { modespeaker obmspvl $* }
		( 4 ) { modespeaker * $* }
		( * ) { modespeaker - $* }
	}
}

alias modespeaker {
	@:_tts.mn = (left(1 $3)==[+]) ? [plus $right(1 $3)] : [minus $right(1 $3)]
	switch ( $3 ) {
		( +b ) ( -b ) {
			@:_tts.mw = ([$3]==[+b]) ? [banned] : [unbanned]
			if (match($4 $N!$X)) {
				@:_tts.critmode = [$ts($1) $_tts.mw you on $ts($2) with mask $4]
			} else {
				@:_tts.mode = [$ts($1) $_tts.mw mask $4 on $ts($2)]
			}
		}
		( +o ) ( -o ) {
			@:_tts.mw = ([$3]==[+o]) ? [opped] : [de-opped]
			if ([$4]==N) {
				@:_tts.critmode = [$ts($1) $_tts.mw you on $ts($2)]
			} else {
				@:_tts.mode = [$ts($1) $_tts.mw $ts($4) on $ts($2)]
			}
		}
		( +i ) {@:_tts.crtimode = [$ts($1) made $ts($2) invite only]}
		( -i ) {@:_tts.critmode = [$ts($1) made $ts($2) no longer invite only]}
		( +k ) {@:_tts.critmode = [$ts($1) set $ts($2) channel key to: $4-]}
		( -k ) {@:_tts.critmode = [$ts($1) removed key from $ts($2)]}
		( * ) {
			if ([$0]==[*]||(strip($3 $0)!=[$0]&&!_tts.critmode)) {
				if (!_tts.mode) {
					@:_tts.mode = [$ts($1) set mode $_tts.mn $4- on $ts($2)]
				}
			}
		}
	}
	if (_tts.critmode) {
		nspeak $2 $_tts.critmode
	} else {
		if (_tts.mode) {nspeak $2 $_tts.mode}
	}
}

alias mespeak {
	if (getset(speech_hear_myself)==[ON] && (!ischannel($0) || tts.chattykathy($0))) {
		if (_tts.focus!=[$0]) {nspeak force To $ts($0)}
		@_tts.focus = [$0]
		speak $tts.pickvoice($N) $1-
	}
}

alias mspeak {
	if (_tts.focus != [$0]) {nspeak force From $ts($0)}
	@_tts.focus = [$0]
	@:_tts.voice = tts.pickvoice($0)
	if (_tts.last != [$_tts.voice $1-]) {
		speak $_tts.voice $1-
	}
}

alias cspeak {
	if (tts.chattykathy($0 $1) && !tts.gross($1-)) {
		if (_tts.focus != [$0]) { nspeak $0 On $ts($0) }
		if (!(_tts.src==[$1] && _tts.focus==[$0])) { nspeak $0 By $ts($1) }
		@_tts.focus = [$0]
		@_tts.src = [$1]
		speak $tts.pickvoice($1 $0) $2-
	}
}

alias nspeak {
	 if (tts.chattykathy($*)) {
		speak $getset(speech_default_notice_voice) $1-
	}
}

alias tts.addpattern {
	tts.load
	push tts.patterns $encode($*)
	tts.save
}

alias tts.addsub {
	tts.load
	@tts[subs][$encode($tolower($0))] = [$1-]
	xecho -b Added TTS sub for $0 to $1-
	tts.save
}

alias tts.addsrc {
	if (match($0 $_tts.ttssrc)) {
		@_tts.ttssrc = remw($0 $_tts.ttssrc)
	} else {
		push _tts.ttssrc $0
	}
	xecho -b Public text will be spoken from: $_tts.ttssrc
}

alias gross {
	tts.load
	xecho -b Added pattern(s): $*
	push tts.grosspats $*
	tts.save
}

alias vmap.remove {
	@:vmap = tts.vmapmake($*)
	# Remove redundant vmaps
	fe ($tts.vmaps) vm {
		fe ($vmap) vm2 {
			if (match($before(= $vm2) $before(= $vm))) {
				xecho -b Removing existing/redudant $vm vmap
				@tts.vmaps = remw($vm $tts.vmaps)
			}
		}
	}
}

alias vmap {
	if ([$1]) {
		tts.load
		@:vmap = tts.vmapmake($*)
		vmap.remove $0
		if (vmap) {
			push tts.vmaps $vmap
			xecho -b Mapped $0 to voice: $1
		}
		tts.save
	} else {
		vmap.list $0
	}
}

alias vmap.list {
	if ([$0]) {
		@:_tts.vmuh = (match(*!*@* $0)) ? [$0] : [$0!$tts.uhost($0)]
	}

	@:_tts.vmlen = 0
	fe ( $tts.vmaps ) vm { if (@vm > _tts.vmlen) {@_tts.vmlen = @vm}}
	fe ( $tts.vmaps ) vm {
		if (match($before(= $vm) $_tts.vmuh) || ![$0]) {
			@:_tts.vmask = before(= $vm)
			@:_tts.vmvoice = after(= $vm)
			xecho -b $[${_tts.vmlen + 3}]before(= $vm) $after(= $vm)
		}
	}
}

alias tts.load {
	if (fexist($getset(speech_rc_file))) { load $getset(speech_rc_file) }
}
alias tts.save {
	if ((:_tts.savefd = open("$getset(speech_rc_file)" w)) > -1) {
		xecho -b Saving TTS settings to $getset(speech_rc_file)
		fe ( $symbolctl(pmatch assign tts.*) ) _tts.varname {
			@write($_tts.savefd ASSIGN $_tts.varname $uniq(${$_tts.varname}))
		}
		fe ( $symbolctl(pmatch builtin_variable speech_*) ) _tts.setname {
			@write($_tts.savefd SET $_tts.setname $getset($_tts.setname))
		}
	} else {
		xecho -b Could not open() $getset(speech_rc_file) for writing
	}
	@close($_tts.savefd)
}

## Functions

alias tts.uhost {
	@:_tts.uhost = userhost($0)
	if (_tts.uhost==[<UNKNOWN>@<UNKNOWN>]) {
		wait for { ^userhost $0 -cmd {bless;@_tts.uhost = [$3@$4]} }
	}
	if (_tts.uhost!=[<UNKNOWN>@<UNKNOWN>]) {
		return $_tts.uhost
	}
}

alias tts.chattykathy {
	if (getset(speech_speak_all_channels)==[ON] || common($strip(/ $*)/force $_tts.ttssrc)) {
		return 1
	} else {
		return 0
	}
}

# Returns true if $* is judged to be gross
alias tts.gross {
	if (rmatch($1 hi hiya hey* *ello) && [$2]!=N && ![$4])
		{ return 1 }
	fe ($tts.grosspats) gross {
		if ([$1-] =~ gross) { return 1 }
	}
	return 0
}

alias tts.pickvoice {
	if ([$0]==N && getset(speech_default_self_voice)) {
		return $getset(speech_default_self_voice)
	}
	@:_tts.nuh = match(*!*@* $0) ? [$0] : [$0!$userhost()]
	fe ($tts.vmaps) vm {
		if (match($before(= $vm) $_tts.nuh)) {
			return $after(= $vm)
		}
	}
	if ([$1]) {
		return $getset(speech_default_voice)
	} else {
		return $getset(speech_default_msg_voice)
	}
}

alias tts.vmapmake {
	if (match(*!*@* $0)) {
		return $0=$1
	} else {
		if (:guh = tts.uhost($0)) {
			return $0!*@*=$1 $mask(3 $0!$guh)=$1
		} else {
			return $0!*@*=$1
		}
	}
}

alias ts {
	^local _tts.tsret
	fe ( $* ) _tts.ts {
		@:_tts.enc = encode($tolower($strip(= $_tts.ts)))
		if (:_tts.rc = tts[subs][$_tts.enc]) {
			push _tts.tsret $_tts.rc
		} else {
			push _tts.tsret $tts.patsub($_tts.ts)
		}
	}
	return $_tts.tsret
}

alias tts.patsub {
	@:_tts.line = [$*]
	fe ( $tts.patterns ) _tts.pat {
		@:_tts.line = sar(g$decode($_tts.pat)$_tts.line)
	}
	return $_tts.line
}

# load defaults
tts.load

# Written by wuf
