1/* 2 * Copyright (C) 2011 Michal Hruby <michal.mhr@gmail.com> 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program; if not, write to the Free Software 16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 17 * 18 * Authored by Michal Hruby <michal.mhr@gmail.com> 19 * 20 */ 21 22namespace Synapse 23{ 24 public class LaunchpadPlugin : Object, Activatable, ItemProvider //, Configurable, ActionProvider 25 { 26 public bool enabled { get; set; default = true; } 27 28 private LaunchpadAuthObject? auth_object; 29 30 public void activate () 31 { 32 //auth_object = new LaunchpadAuthObject (); 33 } 34 35 public void deactivate () 36 { 37 auth_object = null; 38 } 39 40 public Gtk.Widget create_config_widget () 41 { 42 var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 43 box.show (); 44 45 var authorize_button = new Gtk.Button.with_label (_("Authorize with Launchpad")); 46 authorize_button.show (); 47 box.pack_start (authorize_button, true, false); 48 49 var spinner = new Gtk.Spinner (); 50 box.pack_start (spinner); 51 52 var label = new Gtk.Label (_("Please press the Finish button once you login to Launchpad with your web browser")); 53 label.set_width_chars (40); 54 label.set_line_wrap (true); 55 var proceed_button = new Gtk.Button.with_label (_("Finish authorization")); 56 box.pack_start (label); 57 box.pack_start (proceed_button, true, false); 58 59 /* 60 HashTable<string, string>? step1_result = null; 61 62 // i'm quite sure this leaks as hell, but it works :) 63 authorize_button.clicked.connect (() => { 64 authorize_button.hide (); 65 spinner.show (); 66 spinner.start (); 67 auth_object.auth_step1.begin ((obj, res) => { 68 // FIXME: handle error 69 step1_result = auth_object.auth_step1.end (res); 70 auth_object.auth_step2 (step1_result.lookup ("oauth_token")); 71 Timeout.add_seconds (5, () => { 72 spinner.hide (); 73 spinner.stop (); 74 label.show (); 75 proceed_button.show (); 76 77 return false; 78 }); 79 }); 80 }); 81 82 proceed_button.clicked.connect (() => { 83 proceed_button.hide (); 84 label.hide (); 85 spinner.show (); 86 spinner.start (); 87 auth_object.auth_step3.begin (step1_result.lookup ("oauth_token"), 88 step1_result.lookup ("oauth_token_secret"), 89 (obj, res) => { 90 spinner.hide (); 91 try 92 { 93 var step3_result = auth_object.auth_step3.end (res); 94 message ("token: %s", step3_result.lookup ("oauth_token")); 95 message ("token_secret: %s", step3_result.lookup ("oauth_token_secret")); 96 97 label.set_text (_("Successfully authenticated")); 98 } 99 catch (Error e) 100 { 101 label.set_text (_("Authentication failed") + " (%s)".printf (e.message)); 102 } 103 104 label.show (); 105 }); 106 }); 107 */ 108 109 return box; 110 } 111 112 private class LaunchpadAuthObject : Object 113 { 114 const string CONSUMER_KEY = "Synapse.LaunchpadPlugin"; 115/* 116 private Rest.Proxy proxy; 117 118 protected HashTable<string, string> parse_form_reply (string payload) 119 { 120 var ht = new HashTable<string, string> (str_hash, str_equal); 121 122 string[] parameters = payload.split ("&"); 123 foreach (unowned string p in parameters) 124 { 125 string[] parameter = p.split ("=", 2); 126 ht.insert (parameter[0], parameter[1]); 127 } 128 129 return ht; 130 } 131 132 private class Credentials : ConfigObject 133 { 134 public string token { get; set; default = ""; } 135 public string token_secret { get; set; default = ""; } 136 } 137 138 private Credentials creds; 139 140 construct 141 { 142 // make sure we keep a ref to this, otherwise it'll crash when the call 143 // finishes 144 proxy = new Rest.Proxy ("https://launchpad.net/", false); 145 146 creds = ConfigService.get_default ().bind_config ( 147 "plugins", "launchpad-plugin", typeof (Credentials) 148 ) as Credentials; 149 } 150 151 public bool is_authenticated () 152 { 153 return creds.token != "" && creds.token_secret != ""; 154 } 155 156 public void get_tokens (out string token, out string token_secret) 157 { 158 token = creds.token; 159 token_secret = creds.token_secret; 160 } 161 162 public async HashTable<string, string> auth_step1 () throws Error 163 { 164 Error? err = null; 165 166 var call = proxy.new_call (); 167 168 call.set_method ("POST"); 169 call.set_function ("+request-token"); 170 call.add_param ("oauth_consumer_key", CONSUMER_KEY); 171 call.add_param ("oauth_signature_method", "PLAINTEXT"); 172 call.add_param ("oauth_signature", "&"); 173 174 call.run_async ((call_obj, error, obj) => { 175 err = error; 176 auth_step1.callback (); 177 }, this); 178 yield; 179 180 if (err != null) throw err; 181 182 // the reply should have oauth_token & oauth_token_secret 183 var result = parse_form_reply (call.get_payload ()); 184 return result; 185 } 186 187 public void auth_step2 (string oauth_token) 188 { 189 // https://launchpad.net/+authorize-token?oauth_token={oauth_token} 190 Utils.open_uri ("https://launchpad.net/+authorize-token?oauth_token=" + oauth_token); 191 } 192 193 public async HashTable<string, string> auth_step3 (string oauth_token, 194 string token_secret) throws Error 195 { 196 Error? err = null; 197 198 var call = proxy.new_call (); 199 call.set_method ("POST"); 200 call.set_function ("+access-token"); 201 call.add_param ("oauth_token", oauth_token); 202 call.add_param ("oauth_consumer_key", CONSUMER_KEY); 203 call.add_param ("oauth_signature_method", "PLAINTEXT"); 204 call.add_param ("oauth_signature", "&" + token_secret); 205 206 call.run_async ((call_obj, error, obj) => { 207 err = error; 208 auth_step3.callback (); 209 }, this); 210 yield; 211 212 if (err != null) throw err; 213 214 // the reply should have new oauth_token & oauth_token_secret 215 var result = parse_form_reply (call.get_payload ()); 216 creds.token = result.lookup ("oauth_token") ?? ""; 217 creds.token_secret = result.lookup ("oauth_token_secret") ?? ""; 218 219 return result; 220 } 221*/ 222 } 223 224 private class LaunchpadObject : UriMatch 225 { 226 public LaunchpadObject (string title, string desc, string uri) 227 { 228 Object (title: title, description: desc, 229 icon_name: ContentType.get_icon ("text/html").to_string (), 230 uri: uri, mime_type: "text/html", 231 file_type: QueryFlags.INTERNET); 232 } 233 } 234 235 static void register_plugin () 236 { 237 PluginRegistry.get_default ().register_plugin ( 238 typeof (LaunchpadPlugin), 239 "Launchpad", 240 _("Find bugs and branches on Launchpad."), 241 "applications-internet", 242 register_plugin 243 ); 244 } 245 246 static construct 247 { 248 register_plugin (); 249 } 250 251 private Regex bug_regex; 252 private Regex branch_regex; 253 254 construct 255 { 256 try 257 { 258 bug_regex = new Regex ("(?:bug|lp|#):?\\s*#?\\s*(\\d+)$", RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); 259 branch_regex = new Regex ("lp:(~?[a-z]+[+-/_a-z0-9]*)", RegexCompileFlags.OPTIMIZE); 260 } 261 catch (RegexError err) 262 { 263 warning ("Unable to construct regex: %s", err.message); 264 } 265 } 266 267 public bool handles_query (Query q) 268 { 269 return (QueryFlags.INTERNET in q.query_type || QueryFlags.ACTIONS in q.query_type); 270 } 271 272 public async ResultSet? search (Query q) throws SearchError 273 { 274 string? uri = null; 275 string title = null; 276 string description = null; 277 var result = new ResultSet (); 278 279 string stripped = q.query_string.strip (); 280 if (stripped == "") return null; 281 282 MatchInfo mi; 283 if (branch_regex.match (stripped, 0, out mi)) 284 { 285 string branch = mi.fetch (1); 286 string[] groups = branch.split ("/"); 287 if (groups.length == 1) 288 { 289 // project link (lp:synapse) 290 uri = "https://code.launchpad.net/" + branch; 291 title = _("Launchpad: Bazaar branches for %s").printf (branch); 292 description = uri; 293 } 294 else if (groups.length == 2 && !branch.has_prefix ("~")) 295 { 296 // series link (lp:synapse/0.3) 297 uri = "https://code.launchpad.net/" + branch; 298 title = _("Launchpad: Series %s for Project %s").printf (groups[1], groups[0]); 299 description = uri; 300 } 301 else if (branch.has_prefix ("~")) 302 { 303 // branch link (lp:~mhr3/synapse/lp-plugin) 304 uri = "https://code.launchpad.net/" + branch; 305 title = _("Launchpad: Bazaar branch %s").printf (branch); 306 description = uri; 307 } 308 309 if (uri != null) 310 { 311 result.add (new LaunchpadObject (title, description, uri), 312 MatchScore.EXCELLENT); 313 } 314 } 315 else if (bug_regex.match (stripped, 0, out mi)) 316 { 317 string bug_num = mi.fetch (1); 318 319 uri = "https://bugs.launchpad.net/bugs/" + bug_num; 320 title = _("Launchpad: Bug #%s").printf (bug_num); 321 description = uri; 322 result.add (new LaunchpadObject (title, description, uri), 323 MatchScore.ABOVE_AVERAGE); 324 } 325 326 q.check_cancellable (); 327 return result; 328 } 329 330 public ResultSet? find_for_match (ref Query query, Match match) 331 { 332 return null; 333 } 334 } 335} 336