summaryrefslogtreecommitdiffstats
path: root/discord.tcl
blob: 5cc8b80efdb1dd09f71faef13524e06e8e0c191a (plain) (blame)
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
#!/usr/bin/tclsh
source www.tcl
namespace eval discord {

	package require Tcl 8.4
	package require http 2.7
	package require logger
	package require sha1
	package require base64
	package require websocket 1.3.1
	::websocket::loglevel "debug"

	package require tls
	http::register https 443 [list ::tls::socket -autoservername true]

	package require Tcl 8.5
	package require json::write 1.0.3

	package require Tcl 8.4
	package require json 1.3.3

	package require TclOO

	package require logger

	package require lambda

	#unused
	proc escape {string} {
		set r ""
		foreach t [split $string ""] {
			if [regexp {[[:print:]]} $t] {
				append r $t
			} else {
				append r "\\x[format %02X [scan $t %c]]"
			}
		}
		return $r
	}

	proc every {ms body} {
		if {[catch $body] != 0} {
			return
		}
		after $ms [namespace code [info level 0]]
	}

	set html_mapping { "\"" &quot; ' &apos; & &amp; < &lt; > &gt; }

	proc login {email password callback {captcha {}}} {
		if {$captcha == {}} {
			set capt null
		} else {
			set capt [::json::write string $captcha]
		}
		proc login_command {callback http_token} {
			upvar #0 $http_token state
			if {[catch {
				set discord_token [dict get [::json::json2dict $state(body)] token]
				set user_id [dict get [::json::json2dict $state(body)] user_id]
			} result] != 0} {
				if {[catch {
					set captcha_sitekey [dict get [::json::json2dict $state(body)] captcha_sitekey]
				} result] != 0} {
					if {[catch {
						set message [dict get [lindex [dict get [::json::json2dict $state(body)] errors login _errors] 0] message]
					} result] != 0} {
						{*}$callback error $result $state(body)
					} else {
						{*}$callback error_message $message $state(body)
					}
				} else {
					{*}$callback captcha $captcha_sitekey $state(body)
				}
			} else {
				{*}$callback ok $discord_token $user_id
			}
			::http::cleanup $http_token
		}
		::http::geturl https://discord.com/api/v9/auth/login -query "{\"login\":[::json::write string $email],\"password\":[::json::write string $password],\"undelete\":\"false\",\"captcha_key\":$capt,\"login_source\":null,\"gift_code_sku_id\":null}" -timeout 10000 -type application/json -command "[namespace which login_command] {$callback}"
	}
	
	proc connect_example_callback {type {arg1 1}} {
		switch $type {
			ok {
				puts stderr "connect success!"
			}
			authfail {
				puts stderr "auth failed, try login again!"
			}
			error {
				puts stderr "error connecting: $arg1"
			}
		}
	}

	proc login_example_callback {type {arg1 {}}} {
		switch $type {
			ok {
				puts "ok, login successful"
			}
			captcha {
				puts "solve the captcha at address $arg1"
			}
			error_message {
				puts "the server sent a human-readable error message: $arg1"
			}
			error {
				puts "error: $arg1"
			}
		}
	}

	# links sent in mail are click.discord.com links, I couldn't reverse engineer the upn param
	proc authorize_ip {click_url callback} {
		proc authorize_ip_command {callback http_token} {
			upvar #0 $http_token state
			dict for {key value} $state(meta) {
				dict append headers [string tolower $key] $value
			}
			proc authorize_ip_post_command {callback http_token} {
				upvar #0 $http_token state
				if {[lindex [::http::code $http_token] 1] <= 299 && [lindex [::http::code $http_token] 1] >= 200} {
					{*}$callback ok
				} else {
					set code {}
					if {[catch {
						set message [dict get [::json::json2dict $state(body)] message]
						set code [dict get [::json::json2dict $state(body)] code]
					}] != 0} {
						{*}$callback error $state(body)
					} else {
						if {$code == 50014} {
							# expired token
							{*}$callback invalid_token $message
						} else {
							{*}$callback error $state(body)
						}
					}
				}
				::http::cleanup $http_token
			}
			::http::geturl https://discord.com/api/v9/auth/authorize-ip -query "{\"token\":[::json::write string [lindex [split [dict get $headers location] "="] 1]]}" -timeout 10000 -type application/json -command [list [namespace which authorize_ip_post_command] $callback]
			::http::cleanup $http_token
		}
		::http::geturl $click_url -timeout 10000 -command "[namespace which authorize_ip_command] {$callback}"
	}
	proc authorize_ip_example_callback {type {arg1 {}}} {
		switch $type {
			ok {
				puts "ip authorized"
			}
			invalid_token {
				puts "invalid, possibly expired, token. message from server: $arg1"
			}
			error {
				puts "unable to parse response from server: $arg1"
			}
		}
	}
	oo::class create discord {
		constructor {{stor {login {} password {} token {} user_id {}}}} {
			my variable log storage
			set storage $stor
			set log [logger::init discord]
		}
		destructor {
			my variable log storage
			if {[my is_connected] != -1} {
				my disconnect
			}
			${log}::delete
			return storage
		}
		method disconnect {} {
			my variable sock
			::websocket::close $sock 1000 "Tcl/Tk discord odjemalec se poslavlja"
			unset sock
		}
		method set_login_password {login password} {
			my variable storage
			dict set storage login $login
			dict set storage password $password
		}
		method set_token {token} {
			my variable storage
			dict set storage token
		}
		# handles captcha interactively
		method login {callback {captcha {}}} {
			my variable storage log
			proc login_callback {self_discordobj callback type {arg1 ""} {arg2 ""}} {
				my variable storage log
				switch $type {
					ok {
						${log}::notice "login ok: token is $arg1, user_id is $arg2"
						dict set storage token $arg1
						dict set storage user_id $arg2
						{*}$callback ok [list $arg1 $arg2]
					}
					captcha {
						${log}::warn "login captcha: sitekey is $arg1"
						proc captcha.html {sitekey client path arguments headers body uri} {
							global argv0
							$client send {200 ok} {content-type text/html} "<h1><code>$argv0</code> captcha</h1>
							<p>please solve the captcha below in order to login.</p>
							<p>after solving the captcha, press the button under the captcha for submitting the form.</p>
							<p>you need to have javascript support for captcha rendering</p>
							<form method=post action=submit>
								<script src=https://js.hcaptcha.com/1/api.js async defer></script>
								<div class=h-captcha data-sitekey='[string map $::discord::html_mapping $sitekey]'></div>
								<input type=submit />
							</form>
"
						}
						proc submit {discordobj callback server client path arguments headers body uri} {
							if {![dict exists $arguments h-captcha-response]} {
								return $client send {400 bad request} {content-type text/html} "<h1>failed to obtain captcha response</h1>
								<p>your browser did not send the captcha response token</p>
								<p>check that javascript is enabled and that the captcha did not show any errors</p>
								<p>also make sure that no content blockers are interfering with the captcha rendering that that the captcha was solved successfully (green tick)</p>
								<h2>make a decision</h2>
								<p><a href=/captcha.html>&lt;== you can try again by clicking here and going back</a></p>
								<p><a href=/stop.txt>or press here to free resources of the http server</a></p>
"
							}
							global argv0
							$client send {200 ok} {content-type text/plain} "captcha token received. please close this browser tab and proceed to the $argv0 application
"
							# this keeps the client object alive
							$server destroy
							{*}$discordobj login $callback [dict get $arguments h-captcha-response]
						}
						proc stop.txt {server client path arguments headers body uri} {
							# server destroy does not destroy clients
							$server destroy
							$client send {200 ok} {content-type text/plain} "http server resources were freed. please close this browser tab.
"
						}
						::www::server create s 0 [list /captcha.html [list [namespace which captcha.html] $arg1] /submit [list [namespace which submit] $self_discordobj $callback [namespace current]::s] /stop.txt [list [namespace which stop.txt] [namespace current]::s]]
						${log}::notice "please solve captcha at http://127.0.0.1:[s ports]/captcha.html"
						{*}$callback captcha "http://127.0.0.1:[s ports]/captcha.html"
					}
					error_message {
						${log}::error "server sent a human-readable error message: $arg1, response from server is $arg2"
						{*}$callback error_message $arg1
					}
					error {
						${log}::error "login error: message is $arg1, response from server is $arg2"
						{*}$callback error [list $arg1 $arg2]
					}
				}
			}
			::discord::login [dict get $storage login] [dict get $storage password] [list [namespace which login_callback] [self] $callback] $captcha
		}
		# websocket complains that it couldn't remove socket from socketmap in ::http, but future connections to same host and port are working regardless. if something doesn't work in this direction, this is likely the cause -- 2022-08-09
		method connect {} {
			my variable sock log storage
			if {[my is_connected] != -1} {
				my disconnect
			}
			${log}::notice "trying to connect"
			proc handler { sock type msg } {
				my variable log last_packet
				switch $type {
					text {
						${log}::debug "received a message: $msg"
						set p [::json::json2dict $msg]
						set last_packet [dict get $p s]
						if [dict exists $p d heartbeat_interval] {
							set heartbeat_interval [dict get $p d heartbeat_interval]
						}
					}
					connect {
						${log}::notice "connected"
					}
					disconnect {
						${log}::notice "disconnected"
					}
					close {
						${log}::notice "pending closure of connection"
					}
					# binary and ping are unsupported
					default {
						${log}::warn "received an unsupported handler call. type is $type, msg is $msg"
					}
				}
			}
			# in order to overwrite the user-agent header
			set sock [::websocket::open "wss://gateway.discord.gg/?encoding=json&v=9" [namespace which handler] -headers {Origin https://discord.com User-Agent "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"}]
			${log}::debug "created sock, $sock"
		}
		method is_connected {} {
			my variable sock log
			if {![info exists sock]} {
				return -1
			}
			if {[::websocket::conninfo $sock state] == "CONNECTED"} {
				return [::websocket::conninfo $sock peername]
			}
			return 0
		}
	}
}
if [string match *discord.tcl* $argv0] {
	::discord::discord create d
	d set_login_password $env(DC_E) $env(DC_P)
	proc login_callback {dobj type {arg1 {}}} {
		switch $type {
			ok {
				puts "ok, login successful"
				$dobj connect
			}
			captcha {
				puts "solve the captcha at address $arg1"
			}
			error_message {
				puts "the server sent a human-readable error message: $arg1"
			}
			error {
				puts "error: $arg1"
			}
		}
	}
	d login [list [namespace which login_callback] [namespace which d]]
	after 10000 set end 1
	vwait end
	vwait forever
	::discord::login env(DC_E) env(DC_P) login_example_callback
	d connect
	gets stdin
	d disconnect
	gets stdin
	d destroy
}