summaryrefslogtreecommitdiffstats
path: root/modules/sed.bash
blob: b75a2528b765127c3fd51d3815427462e949f705 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# modules/sed.bash
#
# Provides sedbot.
#
# Settings:
# SED_TIMEOUT_BIN: path to the timeout script¹; optional to prevent regex DoS
# SED_TIMEOUT_MEM: maximum amount of memory (in kilobytes) a sed process may use
#
# ¹: https://github.com/pshved/timeout


if [ -z "$IRCBOT_MODULE" ]; then
	printf "error: %s is a module for ircbot.bash and should not be run separately\n" "$0"
	exit 1
fi

declare -g -A sed_messages

# seds a previous message if instucted to
on_privmsg() { # args: $1 - source, $2 - channel/target, $3 - message
	local where nick msg
	local targetednick origmsg origkey
	local key target fromnick regexed ctcp

	where="$2"
	nick="$(parse_source_nick "$1")"
	msg="$3"
	if [[ $where == "$IRCBOT_NICK" ]]; then
		where="$nick"
	fi

	# targeted means the message was ``<user1> user2: s/foo/bar/'' (targets user2's last message)
	origmsg="$msg"
	origkey="$where $nick"
	if targetednick="$(parse_targeted_nick "$msg")"; then
		nick="$targetednick"
		msg="$(parse_targeted_msg "$msg")"
	fi


	key="$where $nick"
	target="${sed_messages[$key]:-}"

	# handle regexing CTCP ACTIONs properly
	ctcp="$(parse_ctcp_command "$target")" || true
	if [[ $ctcp == ACTION ]]; then
		target="$(parse_ctcp_message "$target")"
		fromnick="$(printf "\\x02* %s\\x02" "$nick")"
	else
		fromnick="<$nick>"
	fi

	if regexed="$(sed_replace "$msg" "$target")"; then # if a replacement was done, send it
		sendmsg PRIVMSG "$where" "$fromnick $regexed"
	else # otherwise, store the triggering message
		sed_messages[$origkey]="$origmsg"
	fi
}

sed_replace() { # args: $1 - sed s expression, $2 - text to regex
	local msg target
	local l pos i p del
	local regexps ok t target

	msg="$1"
	target="$2"

	del="${msg:1:1}"
	if [[ -z $del ]] || [[ $(indexof '/|,!:' "$del") -lt 0 ]]; then
		return 1
	fi

	# TODO: rewrite so that only one expression is tokenized at once (allows different delimiters)

	# tokenize
	l=()
	pos="$(indexof "$msg" "$del")"
	while ((pos >= 0)); do
		i=0
		((p=pos-i-1))
		while ((p >= 0)) && [[ ${msg:$p:1} == '\' ]]; do # count \ characters
			((i++))
			((p=pos-i-1))
		done
		if ((i%2 == 0)); then
			l+=("${msg:0:$pos}")
			((p=pos+1))
			msg="${msg:$p}"
			pos=0
		else
			((pos++))
		fi
		p="$(indexof "${msg:$pos}" "$del")"
		if ((p >= 0)); then
			((pos+=p))
		else
			pos=$p
		fi
	done
	l+=("$msg")

	# l is now an array of the expr separated by unescaped delimiters

	i=0
	regexps=()
	ok=1

	# s/expr1/repl1/opts1 s/expr2/repl2/opts2 s/expr3/repl3
	while ((i < ${#l[@]})); do
		# begins with s
		if [ "${l[$i]}" != "s" ]; then
			break
		fi
		((i++))

		# expr
		if ((i >= ${#l[@]})); then
			break
		fi
		exp="${l[$i]}"
		((i++))

		# repl
		if ((i >= ${#l[@]})); then
			break
		fi
		repl="${l[$i]}"
		((i++))

		# opts
		opts=''
		if ((i < ${#l[@]})); then
			opts="${l[$i]}"
			p=0
			while ((p < ${#opts})); do
				c="${opts:$p:1}"
				if ! [[ $c =~ ^[ig0-9]$ ]]; then # allowed opts are 0-9, i and g
					ok=0
					break
				fi
				((p++))
			done
			if ! ((ok)); then
				# multiple regexps per line
				if [[ "${opts:$p:1}" == ' ' || "${opts:$p:1}" == ';' ]]; then # expression separators are space and ;
					p1=$p
					while ((p < ${#opts})); do
						if [[ "${opts:$p:1}" != ' ' && "${opts:$p:1}" != ';' ]]; then
							break
						fi
						p=$((p+1))
					done
					l[$i]="${opts:$p}"
					opts="${opts:0:$p1}"
					ok=1
				else
					break
				fi
			fi
		fi

		if ((ok)); then
			regexps+=("s$del$exp$del$repl$del$opts")
		fi
	done


	if ! ((ok)) || ((${#regexps[@]} == 0)); then
		return 1
	fi

	t="$target"
	for re in "${regexps[@]}"; do
		if [[ -n ${SED_TIMEOUT_BIN:-} ]]; then
			target="$("$SED_TIMEOUT_BIN" -m "$SED_TIMEOUT_MEM" sed -e "$re" <<< "$target")"
		else
			target="$(sed -e "$re" <<< "$target")"
		fi
	done
	target="$(trimrn <<< "$target")"
	verbose "sed '%s' <<< '%s' >>> '%s'" "$1" "$2" "$target"
	if [[ $target != "$t" ]]; then
		if [[ -n $target ]]; then
			trimrn <<< "$target"
			return 0
		fi
	fi
	return 1
}