├── .gitignore ├── CHANGES ├── LICENSE ├── README.markdown ├── project.clj └── src ├── twitter.clj └── twitter └── query.clj /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | pom.xml 3 | *.jar 4 | classes -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | * 1.2.5 2 | 3 | - Updated dependency of clj-oauth to 1.2.10. 4 | 5 | * 1.2.4 6 | 7 | - Updated dependency of clj-oauth to 1.2.9. 8 | 9 | * 1.2.3 10 | 11 | - Depending on clj-oauth 1.2.4. 12 | 13 | * 1.2.2 14 | 15 | - Fixed incorrect merging of optional params. Possible regression from 1.2. (found and fixed by Zehua Liu) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Matt Revelle 2 | All rights reserved 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Twitter client API for Clojure # 2 | 3 | Access the Twitter API from Clojure. 4 | 5 | 6 | # Building # 7 | 8 | lein deps 9 | lein jar 10 | 11 | 12 | # Example # 13 | 14 | (require 'twitter 15 | ['oauth.client :as 'oauth]) 16 | 17 | ;; Make a OAuth consumer 18 | (def oauth-consumer (oauth/make-consumer 19 | 20 | "https://api.twitter.com/oauth/request_token" 21 | "https://api.twitter.com/oauth/access_token" 22 | "https://api.twitter.com/oauth/authorize" 23 | :hmac-sha1)) 24 | 25 | (def oauth-access-token 26 | ;; Look up an access token you've stored away after the user 27 | ;; authorized a request token and you traded it in for an 28 | ;; access token. See clj-oauth (http://github.com/mattrepl/clj-oauth) for an example.) 29 | (def oauth-access-token-secret 30 | ;; The secret included with the access token) 31 | 32 | ;; Post to twitter 33 | (twitter/with-oauth oauth-consumer 34 | oauth-access-token 35 | oauth-access-token-secret 36 | (twitter/update-status "posting from #clojure with #oauth")) 37 | 38 | ;; Find out who follows dons 39 | (twitter/followers-of-name "donsbot") 40 | 41 | # Authors # 42 | 43 | Development funded by LikeStream LLC (Don Jackson and Shirish Andhare), see [http://www.likestream.org/opensource.html](http://www.likestream.org/opensource.html). 44 | 45 | Designed and developed by Matt Revelle of [Lightpost Software](http://lightpostsoftware.com). -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-twitter "1.2.8" 2 | :description "Twitter Client API for Clojure" 3 | :dependencies [[org.clojure/clojure "1.5.1"] 4 | [org.clojure/data.json "0.1.2"] 5 | [clj-oauth "1.4.1"] 6 | [org.clojars.tavisrudd/clj-apache-http "2.3.2-SNAPSHOT"]]) 7 | -------------------------------------------------------------------------------- /src/twitter.clj: -------------------------------------------------------------------------------- 1 | (ns twitter 2 | (:use [clojure.data.json :only [read-json]]) 3 | (:require [clojure.set :as set] 4 | [clojure.string :as string] 5 | [com.twinql.clojure.http :as http] 6 | [twitter.query :as query] 7 | [oauth.client :as oauth] 8 | [oauth.signature]) 9 | (:import (java.io File) 10 | (org.apache.http.entity.mime.content FileBody) 11 | (org.apache.http.entity.mime MultipartEntity))) 12 | 13 | (declare status-handler) 14 | 15 | (def ^:dynamic *oauth-consumer* nil) 16 | (def ^:dynamic *oauth-access-token* nil) 17 | (def ^:dynamic *oauth-access-token-secret* nil) 18 | (def ^:dynamic *protocol* "http") 19 | 20 | ;; Get JSON from clj-apache-http 21 | (defmethod http/entity-as :json [entity as state] 22 | (read-json (http/entity-as entity :string state))) 23 | 24 | (defmacro with-oauth 25 | "Set the OAuth access token to be used for all contained Twitter requests." 26 | [consumer access-token access-token-secret & body] 27 | `(binding [*oauth-consumer* ~consumer 28 | *oauth-access-token* ~access-token 29 | *oauth-access-token-secret* ~access-token-secret] 30 | (do 31 | ~@body))) 32 | 33 | (defmacro with-https 34 | [ & body] 35 | `(binding [*protocol* "https"] 36 | (do 37 | ~@body))) 38 | 39 | (defmacro def-twitter-method 40 | "Given basic specifications of a Twitter API method, build a function that will 41 | take any required and optional arguments and call the associated Twitter method." 42 | [method-name req-method req-url required-params optional-params handler] 43 | (let [required-fn-params (vec (sort (map #(symbol (name %)) 44 | required-params))) 45 | optional-fn-params (vec (sort (map #(symbol (name %)) 46 | optional-params)))] 47 | `(defn ~method-name 48 | [~@required-fn-params & rest#] 49 | (let [req-uri# (str *protocol* "://" ~req-url) 50 | rest-map# (apply hash-map rest#) 51 | provided-optional-params# (set/intersection (set ~optional-params) 52 | (set (keys rest-map#))) 53 | required-query-param-names# 54 | (map (fn [x#] 55 | (keyword (string/replace (name x#) #"-" "_" ))) 56 | ~required-params) 57 | 58 | optional-query-param-names-mapping# 59 | (map (fn [x#] 60 | [x# (keyword (string/replace (name x#) #"-" "_"))]) 61 | provided-optional-params#) 62 | 63 | query-params#(merge (apply hash-map 64 | (vec (interleave 65 | required-query-param-names# 66 | ~required-fn-params))) 67 | (apply merge 68 | (map (fn [x#] {(second x#) 69 | ((first x#) 70 | rest-map#)}) 71 | optional-query-param-names-mapping#))) 72 | oauth-creds# (when (and *oauth-consumer* 73 | *oauth-access-token*) 74 | (oauth/credentials *oauth-consumer* 75 | *oauth-access-token* 76 | *oauth-access-token-secret* 77 | ~req-method 78 | req-uri# 79 | query-params#))] 80 | (~handler (~(symbol "http" (name req-method)) 81 | req-uri# 82 | :query (merge query-params# 83 | oauth-creds#) 84 | :parameters (http/map->params 85 | {:use-expect-continue false}) 86 | :as :json)))))) 87 | 88 | ;;;; Almost every method, and all functionality, of the Twitter API 89 | ;;;; is defined below with def-twitter-method or a custom function to support 90 | ;;;; special cases, such as uploading image files. 91 | 92 | (def-twitter-method public-timeline 93 | :get 94 | "api.twitter.com/1/statuses/public_timeline.json" 95 | [] 96 | [] 97 | (comp :content status-handler)) 98 | 99 | (def-twitter-method friends-timeline 100 | :get 101 | "api.twitter.com/1/statuses/friends_timeline.json" 102 | [] 103 | [:since-id 104 | :max-id 105 | :count 106 | :page] 107 | (comp :content status-handler)) 108 | 109 | (def-twitter-method user-timeline 110 | :get 111 | "api.twitter.com/1/statuses/user_timeline.json" 112 | [] 113 | [:id 114 | :user-id 115 | :screen-name 116 | :since-id 117 | :max-id 118 | :count 119 | :page] 120 | (comp :content status-handler)) 121 | 122 | (def-twitter-method home-timeline 123 | :get 124 | "api.twitter.com/1/statuses/home_timeline.json" 125 | [] 126 | [:since-id 127 | :max-id 128 | :count 129 | :page 130 | :skip-user 131 | :include-entities] 132 | (comp :content status-handler)) 133 | 134 | (def-twitter-method mentions 135 | :get 136 | "api.twitter.com/1/statuses/mentions.json" 137 | [] 138 | [:since-id 139 | :max-id 140 | :count 141 | :page] 142 | (comp :content status-handler)) 143 | 144 | (def-twitter-method show-status 145 | :get 146 | "api.twitter.com/1/statuses/show.json" 147 | [:id] 148 | [] 149 | (comp :content status-handler)) 150 | 151 | (def-twitter-method update-status 152 | :post 153 | "api.twitter.com/1/statuses/update.json" 154 | [:status] 155 | [:in-reply-to-status-id] 156 | (comp :status :content status-handler)) 157 | 158 | (def-twitter-method destroy-status 159 | :post 160 | "api.twitter.com/1/statuses/destroy.json" 161 | [:id] 162 | [] 163 | (comp :status :content status-handler)) 164 | 165 | (def-twitter-method show-user-by-id 166 | :get 167 | "api.twitter.com/1/users/show.json" 168 | [:user-id] 169 | [] 170 | (comp :content status-handler)) 171 | 172 | (def-twitter-method show-user-by-name 173 | :get 174 | "api.twitter.com/1/users/show.json" 175 | [:screen-name] 176 | [] 177 | (comp :content status-handler)) 178 | 179 | (def-twitter-method lookup-users-by-id 180 | :get 181 | "api.twitter.com/1/users/lookup.json" 182 | [:user-id] 183 | [] 184 | (comp :content status-handler)) 185 | 186 | (def-twitter-method lookup-users-by-name 187 | :get 188 | "api.twitter.com/1/users/lookup.json" 189 | [:screen-name] 190 | [] 191 | (comp :content status-handler)) 192 | 193 | (def-twitter-method direct-messages 194 | :get 195 | "api.twitter.com/1/direct_messages.json" 196 | [] 197 | [:since-id 198 | :max-id 199 | :count 200 | :page] 201 | (comp :content status-handler)) 202 | 203 | (def-twitter-method sent-direct-messages 204 | :get 205 | "api.twitter.com/1/direct_messages/sent.json" 206 | [] 207 | [:since-id 208 | :max-id 209 | :count 210 | :page] 211 | (comp :content status-handler)) 212 | 213 | (def-twitter-method send-direct-message-to-id 214 | :post 215 | "api.twitter.com/1/direct_messages/new.json" 216 | [:user-id 217 | :text] 218 | [] 219 | (comp :content status-handler)) 220 | 221 | (def-twitter-method send-direct-message-to-name 222 | :post 223 | "api.twitter.com/1/direct_messages/new.json" 224 | [:screen-name 225 | :text] 226 | [] 227 | (comp :content status-handler)) 228 | 229 | (def-twitter-method destroy-direct-message 230 | :post 231 | "api.twitter.com/1/direct_messages/destroy.json" 232 | [:id] 233 | [] 234 | (comp :content status-handler)) 235 | 236 | (def-twitter-method create-friendship-to-id 237 | :post 238 | "api.twitter.com/1/friendships/create.json" 239 | [:user-id] 240 | [:follow] 241 | (comp :content status-handler)) 242 | 243 | (def-twitter-method create-friendship-to-name 244 | :post 245 | "api.twitter.com/1/friendships/create.json" 246 | [:screen-name] 247 | [:follow] 248 | (comp :content status-handler)) 249 | 250 | (def-twitter-method destroy-friendship-to-id 251 | :post 252 | "api.twitter.com/1/friendships/destroy.json" 253 | [:user-id] 254 | [] 255 | (comp :content status-handler)) 256 | 257 | (def-twitter-method destroy-friendship-to-name 258 | :post 259 | "api.twitter.com/1/friendships/destroy.json" 260 | [:screen-name] 261 | [] 262 | (comp :content status-handler)) 263 | 264 | (def-twitter-method show-friendship-by-ids 265 | :get 266 | "api.twitter.com/1/friendships/show.json" 267 | [:source-id 268 | :target-id] 269 | [] 270 | (comp :content status-handler)) 271 | 272 | (def-twitter-method show-friendship-by-names 273 | :get 274 | "api.twitter.com/1/friendships/show.json" 275 | [:source-screen-name 276 | :target-screen-name] 277 | [] 278 | (comp :content status-handler)) 279 | 280 | (def-twitter-method friends-of-id 281 | :get 282 | "api.twitter.com/1/friends/ids.json" 283 | [:user-id] 284 | [] 285 | (comp :content status-handler)) 286 | 287 | (def-twitter-method friends-of-name 288 | :get 289 | "api.twitter.com/1/friends/ids.json" 290 | [:screen-name] 291 | [] 292 | (comp :content status-handler)) 293 | 294 | (def-twitter-method followers-of-id 295 | :get 296 | "api.twitter.com/1/followers/ids.json" 297 | [:user-id] 298 | [] 299 | (comp :content status-handler)) 300 | 301 | (def-twitter-method followers-of-name 302 | :get 303 | "api.twitter.com/1/followers/ids.json" 304 | [:screen-name] 305 | [] 306 | (comp :content status-handler)) 307 | 308 | (def-twitter-method verify-credentials 309 | :get 310 | "api.twitter.com/1/account/verify_credentials.json" 311 | [] 312 | [] 313 | (comp :content status-handler)) 314 | 315 | (def-twitter-method rate-limit-status 316 | :get 317 | "api.twitter.com/1/account/rate_limit_status.json" 318 | [] 319 | [] 320 | (comp :content status-handler)) 321 | 322 | (def-twitter-method end-session 323 | :post 324 | "api.twitter.com/1/account/end_session.json" 325 | [] 326 | [] 327 | (comp :content status-handler)) 328 | 329 | (def-twitter-method update-delivery-device 330 | :post 331 | "api.twitter.com/1/account/update_delivery_device.json" 332 | [:device] 333 | [] 334 | (comp :content status-handler)) 335 | 336 | (def-twitter-method update-profile-colors 337 | :post 338 | "api.twitter.com/1/account/update_profile_colors.json" 339 | [] 340 | [:profile-background-color 341 | :profile-text-color 342 | :profile-link-color 343 | :profile-sidebar-fill-color 344 | :profile-sidebar-border-color] 345 | (comp :content status-handler)) 346 | 347 | 348 | (comment (def-twitter-method update-profile-image 349 | :post 350 | "api.twitter.com/1/account/update_profile_image.json" 351 | [:image] 352 | [] 353 | (comp :content status-handler))) 354 | 355 | (defn update-profile-image [^String image] 356 | (let [req-uri__9408__auto__ "http://api.twitter.com/1/account/update_profile_image.json" 357 | 358 | oauth-creds__9414__auto__ (when 359 | (and 360 | *oauth-consumer* 361 | *oauth-access-token*) 362 | (oauth/credentials 363 | *oauth-consumer* 364 | *oauth-access-token* 365 | :post 366 | req-uri__9408__auto__))] 367 | ((comp :content status-handler) 368 | (http/post 369 | req-uri__9408__auto__ 370 | :query 371 | oauth-creds__9414__auto__ 372 | :parameters 373 | (http/map->params {:use-expect-continue false}) 374 | :body (doto (MultipartEntity.) 375 | (.addPart "image" (FileBody. (File. image)))) 376 | :as 377 | :json)))) 378 | 379 | (comment (def-twitter-method update-profile-background-image 380 | :post 381 | "api.twitter.com/1/account/update_profile_background_image.json" 382 | [:image] 383 | [:title] 384 | (comp :content status-handler))) 385 | 386 | (defn update-profile-background-image [^String image & rest__2570__auto__] 387 | (let [req-uri__2571__auto__ 388 | "http://api.twitter.com/1/account/update_profile_background_image.json" 389 | 390 | rest-map__2572__auto__ (apply hash-map rest__2570__auto__) 391 | provided-optional-params__2573__auto__ (set/intersection 392 | (set [:title]) 393 | (set 394 | (keys 395 | rest-map__2572__auto__))) 396 | query-param-names__2574__auto__ (sort 397 | (map 398 | (fn 399 | [x__2575__auto__] 400 | (keyword 401 | (string/replace 402 | (name 403 | x__2575__auto__) 404 | #"-" 405 | "_" 406 | ))) 407 | provided-optional-params__2573__auto__)) 408 | query-params__2576__auto__ (apply 409 | hash-map 410 | (interleave 411 | query-param-names__2574__auto__ 412 | (vec 413 | (vals 414 | (sort 415 | (select-keys 416 | rest-map__2572__auto__ 417 | provided-optional-params__2573__auto__)))))) 418 | oauth-creds__2577__auto__ (when 419 | (and 420 | *oauth-consumer* 421 | *oauth-access-token*) 422 | (oauth/credentials 423 | *oauth-consumer* 424 | *oauth-access-token* 425 | :post 426 | req-uri__2571__auto__ 427 | query-params__2576__auto__))] 428 | ((comp :content status-handler) 429 | (http/post req-uri__2571__auto__ 430 | :query (merge query-params__2576__auto__ oauth-creds__2577__auto__) 431 | :parameters (http/map->params {:use-expect-continue false}) 432 | :body (doto (MultipartEntity.) 433 | (.addPart "image" (FileBody. (File. image)))) 434 | :as :json)))) 435 | 436 | (def-twitter-method update-profile 437 | :post 438 | "api.twitter.com/1/account/update_profile.json" 439 | [] 440 | [:name 441 | :email 442 | :url 443 | :location 444 | :description] 445 | (comp :content status-handler)) 446 | 447 | (def-twitter-method favorites 448 | :get 449 | "api.twitter.com/1/favorites.json" 450 | [] 451 | [:id 452 | :page] 453 | (comp :content status-handler)) 454 | 455 | (def-twitter-method create-favorite 456 | :post 457 | "api.twitter.com/1/favorites/create.json" 458 | [:id] 459 | [] 460 | (comp :content status-handler)) 461 | 462 | (def-twitter-method destroy-favorite 463 | :post 464 | "api.twitter.com/1/favorites/destroy.json" 465 | [:id] 466 | [] 467 | (comp :content status-handler)) 468 | 469 | (def-twitter-method notifications-follow-by-id 470 | :post 471 | "api.twitter.com/1/notifications/follow.json" 472 | [:user-id] 473 | [] 474 | (comp :content status-handler)) 475 | 476 | (def-twitter-method notifications-follow-by-name 477 | :post 478 | "api.twitter.com/1/notifications/follow.json" 479 | [:screen-name] 480 | [] 481 | (comp :content status-handler)) 482 | 483 | (def-twitter-method notifications-leave-by-id 484 | :post 485 | "api.twitter.com/1/notifications/leave.json" 486 | [:user-id] 487 | [] 488 | (comp :content status-handler)) 489 | 490 | (def-twitter-method notifications-leave-by-name 491 | :post 492 | "api.twitter.com/1/notifications/leave.json" 493 | [:screen-name] 494 | [] 495 | (comp :content status-handler)) 496 | 497 | (def-twitter-method create-block 498 | :post 499 | "api.twitter.com/1/blocks/create.json" 500 | [:user-id-or-screen-name] 501 | [] 502 | (comp :content status-handler)) 503 | 504 | (def-twitter-method destroy-block 505 | :post 506 | "api.twitter.com/1/blocks/destroy.json" 507 | [:user-id-or-screen-name] 508 | [] 509 | (comp :content status-handler)) 510 | 511 | (def-twitter-method block-exists-for-id 512 | :get 513 | "api.twitter.com/1/blocks/exists.json" 514 | [:user-id] 515 | [] 516 | (comp :content status-handler)) 517 | 518 | (def-twitter-method block-exists-for-name 519 | :get 520 | "api.twitter.com/1/blocks/exists.json" 521 | [:screen-name] 522 | [] 523 | (comp :content status-handler)) 524 | 525 | (def-twitter-method blocking-users 526 | :get 527 | "api.twitter.com/1/blocks/blocking.json" 528 | [] 529 | [:page] 530 | (comp :content status-handler)) 531 | 532 | (def-twitter-method blocking-user-ids 533 | :get 534 | "api.twitter.com/1/blocks/blocking/ids.json" 535 | [] 536 | [] 537 | (comp :content status-handler)) 538 | 539 | (def-twitter-method saved-searches 540 | :get 541 | "api.twitter.com/1/saved_searches.json" 542 | [] 543 | [] 544 | (comp :content status-handler)) 545 | 546 | (def-twitter-method show-saved-search 547 | :get 548 | "api.twitter.com/1/saved_searches/show.json" 549 | [:id] 550 | [] 551 | (comp :content status-handler)) 552 | 553 | (def-twitter-method create-saved-search 554 | :post 555 | "api.twitter.com/1/saved_searches/create.json" 556 | [:query] 557 | [] 558 | (comp :content status-handler)) 559 | 560 | (def-twitter-method destroy-saved-search 561 | :post 562 | "api.twitter.com/1/saved_searches/destroy.json" 563 | [:id] 564 | [] 565 | (comp :content status-handler)) 566 | 567 | (def-twitter-method users-search 568 | :get 569 | "api.twitter.com/1/users/search.json" 570 | [:q] 571 | [:page 572 | :per_page 573 | :include_entities] 574 | (comp :content status-handler)) 575 | 576 | (def-twitter-method search 577 | :get 578 | "search.twitter.com/search.json" 579 | [:q] 580 | [:callback 581 | :lang 582 | :rpp 583 | :page 584 | :since-id 585 | :max-id 586 | :geocode 587 | :show-user] 588 | (comp :content status-handler)) 589 | 590 | (def-twitter-method trends 591 | :get 592 | "search.twitter.com/trends.json" 593 | [] 594 | [] 595 | (comp :content status-handler)) 596 | 597 | (def-twitter-method current-trends 598 | :get 599 | "search.twitter.com/trends/current.json" 600 | [] 601 | [:exclude] 602 | (comp :content status-handler)) 603 | 604 | (def-twitter-method daily-trends 605 | :get 606 | "search.twitter.com/trends/daily.json" 607 | [] 608 | [:date 609 | :exclude] 610 | (comp :content status-handler)) 611 | 612 | (def-twitter-method weekly-trends 613 | :get 614 | "search.twitter.com/trends/weekly.json" 615 | [] 616 | [:date 617 | :exclude] 618 | (comp :content status-handler)) 619 | 620 | (defn status-handler 621 | "Handle the various HTTP status codes that may be returned when accessing 622 | the Twitter API." 623 | [result] 624 | (condp #(if (coll? %1) 625 | (first (filter (fn [x] (== x %2)) %1)) 626 | (== %2 %1)) (:code result) 627 | 200 result 628 | 304 nil 629 | [400 401 403 404 406 500 502 503] 630 | (let [body (:content result) 631 | headers (into {} (:headers result)) 632 | error-msg (:error body) 633 | error-code (:code result) 634 | request-uri (:request body)] 635 | (throw (proxy [Exception] 636 | [(str "[" error-code "] " error-msg ". [" request-uri "]")] 637 | (request [] (body "request")) 638 | (remaining-requests [] (headers "X-RateLimit-Remaining")) 639 | (rate-limit-reset [] (java.util.Date. 640 | (long (headers "X-RateLimit-Reset"))))))))) 641 | 642 | (defn make-rate-limit-handler 643 | "Creates a handler that will only be called if the API rate limit has been exceeded." 644 | [handler-fn] 645 | (fn [result] 646 | (let [headers (into {} (:headers result))] 647 | (when (= (headers "X-RateLimit-Remaining") "0") 648 | (handler-fn result))))) 649 | -------------------------------------------------------------------------------- /src/twitter/query.clj: -------------------------------------------------------------------------------- 1 | (ns twitter.query 2 | (:refer-clojure :exclude [or name]) 3 | (:require [clojure.string :as string])) 4 | 5 | (defn or 6 | "Build up a search string where all terms are OR'd." 7 | [& terms] 8 | (java.net.URLEncoder/encode 9 | (string/join " OR " 10 | (map #(str "\"" % "\"") terms)) 11 | "UTF-8")) 12 | --------------------------------------------------------------------------------