summaryrefslogtreecommitdiffstats
path: root/discord.tcl
blob: 0548b9b9bdc1b09f28665da633094538a2cf4c58 (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
#!/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
	}

	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
			puts $state(body)
			puts $state(meta)
		}
		puts $click_url
		::http::geturl $click_url -timeout 10000 -command "[namespace which authorize_ip_command] {$callback}"
	}

	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 sockets
			foreach socket $sockets {
				close $socket
			}
			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 log log sockets sockets
				switch $type {
					ok {
						${log}::notice "login ok: token is $arg1, user_id is $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 which 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] "[namespace which login_callback] [self] $callback" $captcha
		}
		method connect {} {
			my variable sock log storage
			if {[my is_connected] != -1} {
				my disconnect
			}
			proc handler { sock type msg } {
				my variable log
				switch $type {
					text {
						${log}::debug "received a message: $msg"
					}
					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"
					}
				}
			}
			set sock [::websocket::open "wss://gateway.discord.gg/?encoding=json&v=9" [namespace which handler]]
			${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::authorize_ip https://click.discord.com/ls/click?upn=qDOo8cnwIoKzt0aLL1cBeJWc43CAiLlKYSQGUErhKV7fF2lroxEuEaMS14HcVQRWKEsXxCWfVqMYFnAqiyFKlyc60qLVSfrR2BpIhn60wAL4y7X8dEY5UhD7n-2BEIulILGfHWzhi03YqYAqwN1dzNDsL7BrPVj5dWSRz43qNCKZs2Mre7Chd4IbpPox9Y-2F4ktYY4N_PdlvdP47mdorwIWlvGoY-2Fnv9MARx98jl0olgQff-2FSKZtFfa9W0dHpN7isUf-2BBQiGTEplWgkKV5AWO17KWsssOLh7AWnsZoy5YvVHlKWck92RQ5vrqN5LMcYUfucbVfjTrOdjPoOJLpa-2F6uIpp4HgFjgpPQjxA-2B3Mm9UmJJTSAUKfWdzMYiWkDqft72DZnIyAHKkjDaEO8wSn3CcCTqm-2FzMMTi-2BVKGxWIQb2p-2F6LNSxtos3YXgYUOtHh5pLu6WeUIvmdhDYWgvG9534NpmGXELQ-3D-3D {}
	vwait forever
	::discord::discord create d
	d set_login_password $env(DC_E) $env(DC_P)
	d login ::discord::login_example_callback
	vwait forever
	::discord::login env(DC_E) env(DC_P) login_example_callback
	d connect
	gets stdin
	d disconnect
	gets stdin
	d destroy
}