summaryrefslogblamecommitdiffstats
path: root/discord.tcl
blob: 5cc8b80efdb1dd09f71faef13524e06e8e0c191a (plain) (tree)







































                                                                          






                                                         

                                                                       





                                                                

                                                          
                                    
                                                                                                   




                                                                                                                       






                                                                                                                                                          
                                        
                                                                                          

                                 
                                                                       
                         
                                                   

























                                                                                                                                                                                                                                                                                                                                                               


                                                                                            





                                                   



                                                                                                   


























                                                                                                                                                                                                                                                                                               
                 

                                                                                                                      












                                                                                                   






                                                                                 
                                               




















                                                                                             
                                                      
                                               
                                                                                                 
                                                       


                                                                                                           

                                                                              
                                                                                  


                                                                                              






















                                                                                                                                                                                                                    
                                                         




                                                                                                                                                                                                    
                                                                                                                               





                                                                                                                                                                         
                                                 
                                                                                                                                                                                                                                                                                                      
                                                                                                                                




                                                                                                                                                


                                                                                                                            
                                                                                     


                                         
                                                                                                                                                                 
                 
                                                                                                                                                                                                                                                       




                                                      
                                                          
                                                        
                                                           


                                                                                        




                                                                                                                 















                                                                                                                               

                                                                                                                                                                                                                                                                                       













                                                                                 
                                        

                                                  



















                                                                                            







                                                                   
#!/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
}