1 module twitter.twitterbase;
2 
3 import twitter.api;
4 
5 import vibe.http.common;
6 
7 /// took some inspiration from: https://github.com/alphaKAI/twitter4d
8 abstract class TwitterBase
9 {
10     private static immutable API_URL = "https://api.twitter.com";
11 
12     private TwitterCredentials credentials;
13 
14     ///
15     this(TwitterCredentials credentials)
16     {
17         this.credentials = credentials;
18     }
19 
20     ///
21     protected auto request(T)(string path, HTTPMethod method, string[string] params)
22     {
23         import std.algorithm : filter, map;
24         import std.string : join;
25         import std.conv : to;
26         import vibe.d : requestHTTP, HTTPClientRequest, deserializeJson,
27             formEncode, readAllUTF8;
28         import vibe.core.log : logError, logInfo, logDebug;
29 
30         immutable url = API_URL ~ path;
31 
32         string[string] paramsObj = buildParams(params);
33         paramsObj["oauth_signature"] = signature(method, url, paramsObj);
34 
35         auto authorizeKeys = paramsObj.keys.filter!q{a.startsWith("oauth_")};
36         auto authorize = "OAuth " ~ authorizeKeys.map!(k => k ~ "=" ~ paramsObj[k]).join(",");
37 
38         string getPath = paramsObj.keys.map!(k => k ~ "=" ~ paramsObj[k]).join("&");
39 
40         auto res = requestHTTP(url ~ '?' ~ getPath, (scope HTTPClientRequest req) {
41 
42             req.method = method;
43             // if (method == HTTPMethod.POST)
44             // {
45             //     req.headers["Content-Type"] = "application/x-www-form-urlencoded";
46             //     req.headers["Content-Length"] = (getPath.length).to!string;
47             // }
48             req.headers["Authorization"] = authorize;
49 
50             //req.bodyWriter.write(getPath);
51         });
52         scope (exit)
53         {
54             res.dropBody();
55         }
56 
57         if (res.statusCode == 200)
58         {
59             auto json = res.readJson();
60 
61             scope (failure)
62             {
63                 logError("Response deserialize failed: %s", json);
64             }
65 
66             return deserializeJson!T(json);
67         }
68         else
69         {
70             logDebug("API Error: %s", res.bodyReader.readAllUTF8());
71             logError("API Error Code: %s", res.statusCode);
72             throw new Exception("API Error");
73         }
74     }
75 
76     private string signature(HTTPMethod method, string url, string[string] params)
77     {
78         import std.digest.sha : SHA1;
79         import std.digest.hmac : hmac;
80         import std.string : representation;
81         import std.algorithm : map, sort;
82         import std.string : join;
83         import std.base64 : Base64;
84 
85         immutable methodString = method == HTTPMethod.GET ? "GET" : "POST";
86 
87         auto query = sort(params.keys).map!(k => k ~ "=" ~ params[k]).join("&");
88         auto key = [this.credentials.consumerSecret, this.credentials.accessTokenSecret].map!(
89                 x => encodeComponent(x)).join("&");
90         auto base = [methodString, url, query].map!(x => encodeComponent(x)).join("&");
91         string oauthSignature = encodeComponent(
92                 Base64.encode(base.representation.hmac!SHA1(key.representation)));
93 
94         return oauthSignature;
95     }
96 
97     private string[string] buildParams(string[string] additionalParam = null)
98     {
99         import std.uuid : randomUUID;
100         import std.conv : to;
101         import std.datetime.systime : Clock;
102 
103         immutable now = Clock.currTime.toUnixTime.to!string;
104 
105         string[string] params = [
106             "oauth_consumer_key" : this.credentials.consumerKey, //
107             "oauth_nonce" : randomUUID().to!string, //
108             "oauth_signature_method" : "HMAC-SHA1", //
109             "oauth_timestamp" : now, //
110             "oauth_token" : this.credentials.accessToken, //
111             "oauth_version" : "1.0"
112         ];
113 
114         if (additionalParam !is null)
115         {
116             foreach (key, value; additionalParam)
117             {
118                 params[key] = value;
119             }
120         }
121 
122         foreach (key, value; params)
123         {
124             params[key] = encodeComponent(value);
125         }
126 
127         return params;
128     }
129 
130     private string encodeComponent(string s)
131     {
132         import std.regex : ctRegex, replaceAll;
133         import std.uri : encodeComponentUnsafe = encodeComponent;
134 
135         char hexChar(ubyte c)
136         {
137             assert(c >= 0 && c <= 15);
138             if (c < 10)
139                 return cast(char)('0' + c);
140             else
141                 return cast(char)('A' + c - 10);
142         }
143 
144         enum InvalidChar = ctRegex!`[!\*'\(\)]`;
145 
146         return s.encodeComponentUnsafe.replaceAll!((s) {
147             char c = s.hit[0];
148             char[3] encoded;
149             encoded[0] = '%';
150             encoded[1] = hexChar((c >> 4) & 0xF);
151             encoded[2] = hexChar(c & 0xF);
152             return encoded[].idup;
153         })(InvalidChar);
154     }
155 }