饭否客户端

锅里有饭

Inspired by fanfou-py

use Digest::HMAC;
use Digest;
use Digest::SHA;
use MIME::Base64;
use URI::Encode;
use URI;
use Cro::HTTP::Client;

#  URL 编码
sub oauth_escape($s){
    return uri_encode_component($s.Str);
}

# 返回当前时间戳
sub oauth_timestamp() {
    return time;
}

# 单次值, 返回一个8位的随机字符串, 防止重复请求
sub oauth_nonce($size=8) {
    return ((1..9).pick for 1..$size).join("");
}

# 返回按一定顺序拼接好的字符串
sub oauth_query(%args) {
    return (sprintf "%s=%s", $_, oauth_escape(~%args{$_}) for %args.keys.sort).join('&');
}

# URL 正规化
sub oauth_normalized_url($url){
    my URI $u .= new($url);
    return sprintf('%s://%s%s', $u.scheme, $u.host, $u.path);
}

class Auth {
    has %.oauth_consumer    is rw = {};
    has %.oauth_token       is rw = {};
    has $.callback          is rw = 'http://localhost:8080/callback';
    has $.auth_host         is rw = 'http://m.fanfou.com';
    has $.form_urlencoded   is rw = 'application/x-www-form-urlencoded';
    has $.base_api_url      is rw = 'http://api.fanfou.com%s.json';
    has $.access_token_url  is rw = 'http://fanfou.com/oauth/access_token';
    has $.request_token_url is rw = 'http://fanfou.com/oauth/request_token';
    has $.authorize_url     is rw = $!auth_host ~ '/oauth/authorize?oauth_token=%s&oauth_callback=%s';

    # 加密算法
    method HMAC_SHA1($key_string, $base_string) {
        return MIME::Base64.encode(hmac($key_string, $base_string, &sha1));
    }

    # 签名值 https://github.com/FanfouAPI/FanFouAPIDoc/wiki/Oauthsignature
    method oauth_signature($url, $method, %base_args) {
        my $normalized_url = oauth_normalized_url($url);
        my $query_items    = oauth_query(%base_args);
        my @base_elems     = ($method.uc, $normalized_url, $query_items);
        my $base_string    = (oauth_escape($_) for @base_elems).join("&");
        my @keys_elems     = (%.oauth_consumer{'secret'}, %.oauth_token{'secret'} // '');
        my $keys_string    = (oauth_escape($_) for @keys_elems).join("&");
        return self.HMAC_SHA1($keys_string, $base_string);
    }

    method oauth_header(%base_args, $realm='') {
        my $auth_header = 'OAuth realm="%s"'.sprintf($realm);
        for %base_args.keys.sort -> $key {
            if $key.starts-with('oauth_') or $key.starts-with('x_auth_') {
                $auth_header ~= ', %s="%s"'.sprintf($key, oauth_escape(%base_args{$key}));
            }
        }
        return {'Authorization' => $auth_header};
    }

    method oauth_request($url is copy, $method='GET', %args={}, %headers={}) {
        my %base_args =  'oauth_consumer_key'     => %!oauth_consumer{'key'},
                         'oauth_signature_method' => 'HMAC-SHA1',
                         'oauth_timestamp'        => oauth_timestamp(),
                         'oauth_nonce'            => oauth_nonce(),
                         'oauth_version'          => '1.0';

        %args = %args.clone;
        %headers = %headers.clone;
        %headers{'User-Agent'} = %headers{'User-Agent'} // 'Cro';

        if $url.starts-with('/') {
           if $url.contains(':') {
                 my ($path,$) = $url.split(':');
                 if %args{'id'} {
                     $url = $path ~ oauth_escape(%args{'id'});
                 } else {
                     $url = $path.chop;
                 }
           }
           $url = $!base_api_url.sprintf($url);
        }

        if $method eq 'POST' {
            %headers{'content-type'} = %headers{'content-type'} // self.form_urlencoded;
            (%headers{'content-type'} eq self.form_urlencoded) and (%base_args ,= %args);
        } else {
            %base_args ,= %args;
            $url = $url ~ '?' ~ oauth_query(%args);
        }

        self.oauth_token and %base_args ,= {'oauth_token' => self.oauth_token{'key'}};
        %base_args{'oauth_signature'} = self.oauth_signature($url, $method, %base_args);
        %headers ,= self.oauth_header(%base_args);

        my $resp;
        my $data = "";
        if (%headers{'content-type'} // '') eq self.form_urlencoded {
            $data = oauth_query(%args);
        } elsif (%headers{'content-type'} // '').contains('form-data') { # multipart/form-data
            $data = %args{'form-data'};
        } else {
            $data = "";
        }

        my $client = Cro::HTTP::Client.new( headers => |%headers );

        if $data {
            $resp = await $client.post: $url,  body => |%args ;
        } else {
            $resp = await $client.get: $url;
        }
        return $resp;
    }
}

class OAuth is Auth {
    method request($url, $method='GET', %args={}, %headers={}) {
        return self.oauth_request($url, $method, %args, %headers);
    }

    method request_token() {
        my $resp = self.oauth_request(self.request_token_url, 'GET');
        my %oauth_token = (await $resp.body-text()).Str.split('&')».split('=').flat.hash;
        self.authorize_url = self.authorize_url.sprintf(%oauth_token{'oauth_token'}, self.callback);
        self.oauth_token = 'key' => %oauth_token{'oauth_token'}, 'secret' => %oauth_token{'oauth_token_secret'};
        return self.oauth_token;
    }

    method access_token(%oauth_token={},$oauth_verifier=Nil) {
        self.oauth_token = %oauth_token or self.oauth_token;
        my %args = do if $oauth_verifier { 'oauth_verifier' => $oauth_verifier } else { {} };
        my $resp = self.oauth_request(self.access_token_url, 'GET', %args);
        %oauth_token = (await $resp.body-text()).Str.split('&')».split('=').flat.hash;
        self.oauth_token = 'key' => %oauth_token{'oauth_token'}, 'secret' => %oauth_token{'oauth_token_secret'};
        return self.oauth_token;
    }
}

class XAuth is Auth {
    has $.username is rw;
    has $.password is rw;
    has %.oauth_token is rw = self.xauth();

    method request($url, $method='GET', %args={}, %headers={}) {
        return self.oauth_request($url, $method, %args, %headers);
    }

    method xauth() {
        my %args =  'x_auth_username' => $!username,
                    'x_auth_password' => $!password,
                    'x_auth_mode'     => 'client_auth';

        my $resp = self.oauth_request(self.access_token_url, 'GET', %args);
        my %oauth_token = (await $resp.body-text()).Str.split('&')».split('=').flat.hash;
        return {'key' => %oauth_token{'oauth_token'}, 'secret' => %oauth_token{'oauth_token_secret'}};
    }

    method access_token() {
        return self.oauth_token;
    }
}
Raku 

comments powered by Disqus