Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

add !search_twitter <query_string> + fix !links

       modified:   fatbot.rb
       new file:   vendor/utils/crack-json.rb
       new file:   vendor/utils/flickraw.rb
       new file:   vendor/utils/twitter_search.rb
  • Loading branch information...
commit 1ad792aa581d7f3fef9bb65a8a7803bda011c944 1 parent fa4c017
@lian lian authored
View
38 fatbot.rb
@@ -4,6 +4,7 @@
# http://jamiedubs.com
#
# dependencies: isaac, sequel, jnunemaker-twitter
+%w{/vendor /vendor/utils}.collect{|ld|$:.unshift File.dirname(__FILE__)+ld}
require 'rubygems'
require 'isaac'
@@ -11,10 +12,13 @@
require 'open-uri' # for !meme
require 'mechanize' # for !swineflu
#gem 'jnunemaker-twitter', :lib => 'twitter' for !twitter
+puts %w{twitter_search flickraw}.collect{|ld|ld+': '+require(ld).to_s}#.join(", ")
+
# require 'sequel'
# DB = Sequel.sqlite('irc.db')
$link_store ||= []
+$twitter ||= TwitterSearch::Client.new
configure do |c|
c.nick = "dubtron"
@@ -60,7 +64,36 @@ def ops?(nick)
cred = YAML.load('twitter.yml')
# TODO do some stuff with twitter gem
msg channel, "*** posting announcement by #{nick} to http://twitter.com/fffffat ..."
-end
+end
+
+
+# ..
+on :channel, /^\!search_twitter (.*)/ do
+ begin
+ case match[0]
+ when /\:all/
+ _query = match[0].gsub(":all",''); _rindex = -1
+ else
+ _query = match[0]; _rindex = 4
+ end
+
+ result = $twitter.query :q => "#{_query}"
+ msg channel, "search_twitter: #{_query} (#{result.size} results)"
+ result[0.._rindex].collect { |i|
+ msg channel, "'#{i.text}' - #{i.from_user} (#{i.time_ago})"
+ }
+ rescue Exception => e
+ msg channel, "search_twitter: (#{e.message}) - twitter timeout."
+ end
+end
+
+# ..
+on :channel, /^\!fatlab_twitter/ do
+ result = $twitter.query :q => "fatlab" # '#fatlab' ?
+ msg channel, "fatlab_twitter: (#{result.size} results)"
+ result.collect { |i| msg channel, "'#{i.text}' - #{i.from_user} (#{i.time_ago})" }
+end
+
# give you a taco. via gerry
# TODO: we need more tacos
@@ -100,7 +133,8 @@ def ops?(nick)
end
on :channel, /^\!(links|bookmarks).*/ do
- msg channel, $link_store.collect { |l| "#{l[:url]} by #{l[:nick]}" }.join("\n")
+ msg channel, "last urls: (#{$link_store.size})"
+ $link_store.collect { |l| msg channel, "#{l[:url]} by #{l[:nick]}" }
end
View
150 vendor/utils/crack-json.rb
@@ -0,0 +1,150 @@
+class Object #:nodoc: Returns true if the object is nil or empty (if applicable)
+ def blank?;nil? || (respond_to?(:empty?) && empty?);end unless method_defined?(:blank?)
+end # class Object
+
+class Numeric #:nodoc: Numerics can't be blank
+ def blank?;false;end unless method_defined?(:blank?)
+end # class Numeric
+
+class NilClass #:nodoc: Nils are always blank
+ def blank?;true;end unless method_defined?(:blank?)
+end # class NilClass
+
+class TrueClass #:nodoc: True is not blank.
+ def blank?;false;end unless method_defined?(:blank?)
+end # class TrueClass
+
+class FalseClass #:nodoc: False is always blank.
+ def blank?;true;end unless method_defined?(:blank?)
+end # class FalseClass
+
+class String #:nodoc:
+ # @example "".blank? #=> true
+ # @example " ".blank? #=> true
+ # @example " hey ho ".blank? #=> false
+ # Strips out whitespace then tests if the string is empty.
+ def blank?;strip.empty?;end unless method_defined?(:blank?)
+ def snake_case
+ return self.downcase if self =~ /^[A-Z]+$/
+ self.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
+ return $+.downcase
+ end unless method_defined?(:snake_case)
+end # class String
+
+class Hash #:nodoc:
+ # @return <String> This hash as a query string
+ # @example
+ # { :name => "Bob",
+ # :address => {
+ # :street => '111 Ruby Ave.',
+ # :city => 'Ruby Central',
+ # :phones => ['111-111-1111', '222-222-2222']
+ # }
+ # }.to_params
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
+ def to_params
+ params = self.map { |k,v| normalize_param(k,v) }.join
+ params.chop! # trailing &
+ params
+ end
+
+ # @param key<Object> The key for the param.
+ # @param value<Object> The value for the param.
+ #
+ # @return <String> This key value pair as a param
+ #
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
+ def normalize_param(key, value)
+ param = ''
+ stack = []
+
+ if value.is_a?(Array)
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
+ elsif value.is_a?(Hash)
+ stack << [key,value]
+ else
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
+ end
+
+ stack.each do |parent, hash|
+ hash.each do |key, value|
+ if value.is_a?(Hash)
+ stack << ["#{parent}[#{key}]", value]
+ else
+ param << normalize_param("#{parent}[#{key}]", value)
+ end
+ end
+ end
+
+ param
+ end
+end
+
+
+# require 'crack/json'
+# Copyright (c) 2004-2008 David Heinemeier Hansson
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+require 'yaml'
+require 'strscan'
+
+module Crack
+ class ParseError < StandardError; end
+ class JSON
+ def self.parse(json)
+ YAML.load(unescape(convert_json_to_yaml(json)))
+ rescue ArgumentError => e
+ raise ParseError, "Invalid JSON string"
+ end
+
+ protected
+ def self.unescape(str)
+ str.gsub(/\\u([0-9a-f]{4})/) { [$1.hex].pack("U") }
+ end
+
+ # matches YAML-formatted dates
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?$/
+
+ # Ensure that ":" and "," are always followed by a space
+ def self.convert_json_to_yaml(json) #:nodoc:
+ scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
+ while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
+ case char = scanner[1]
+ when '"', "'"
+ if !quoting
+ quoting = char
+ pos = scanner.pos
+ elsif quoting == char
+ if json[pos..scanner.pos-2] =~ DATE_REGEX
+ # found a date, track the exact positions of the quotes so we can remove them later.
+ # oh, and increment them for each current mark, each one is an extra padded space that bumps
+ # the position in the final YAML output
+ total_marks = marks.size
+ times << pos+total_marks << scanner.pos+total_marks
+ end
+ quoting = false
+ end
+ when ":",","
+ marks << scanner.pos - 1 unless quoting
+ end
+ end
+
+ if marks.empty?
+ json.gsub(/\\\//, '/')
+ else
+ left_pos = [-1].push(*marks)
+ right_pos = marks << json.length
+ output = []
+ left_pos.each_with_index do |left, i|
+ output << json[left.succ..right_pos[i]]
+ end
+ output = output * " "
+
+ times.each { |i| output[i-1] = ' ' }
+ output.gsub!(/\\\//, '/')
+ output
+ end
+ end
+ end
+end # require 'crack/xml'
View
248 vendor/utils/flickraw.rb
@@ -0,0 +1,248 @@
+# Copyright (c) 2006 Mael Clerambault <maelclerambault@yahoo.fr>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'net/http'
+require 'md5'
+require File.dirname(__FILE__) + "/crack-json.rb" # require 'json'
+require 'cgi'
+
+module FlickRaw
+ VERSION='0.5.1'
+
+ FLICKR_HOST='api.flickr.com'.freeze
+
+ # Path of flickr REST API
+ REST_PATH='/services/rest/?'.freeze
+
+ # Path of flickr auth page
+ AUTH_PATH='/services/auth/?'.freeze
+
+ # Path of flickr upload
+ UPLOAD_PATH='/services/upload/'.freeze
+
+ @api_key = '7b124df89b638e545e3165293883ef62'
+
+ module SimpleOStruct # :nodoc:
+ def __attr_define(k,v)
+ instance_variable_set "@#{k}", v
+ meta = class << self; self; end
+ meta.class_eval { attr_reader k.to_s }
+ end
+ end
+
+ class Response # :nodoc:
+ include SimpleOStruct
+ def initialize(h)
+ h.each {|k, v| __attr_define k, Response.structify(v, k) }
+ end
+
+ def self.structify(obj, name = '')
+ if obj.is_a? Hash
+ if name =~ /s$/ and obj[$`].is_a? Array
+ list = structify obj.delete($`)
+ list.extend SimpleOStruct
+ list.instance_eval { obj.each {|kv, vv| __attr_define kv, vv } }
+ list
+ elsif obj.keys == ['_content']
+ obj['_content'].to_s
+ else
+ Response.new obj
+ end
+ elsif obj.is_a? Array
+ obj.collect {|e| structify e}
+ else
+ obj
+ end
+ end
+
+ def to_s; @_content || super end
+ end
+
+ class FailedResponse < StandardError
+ attr_reader :code
+ alias :msg :message
+ def initialize(msg, code, req)
+ @code = code
+ super("'#{req}' - #{msg}")
+ end
+ end
+
+ class Request
+ def initialize(flickr = nil) # :nodoc:
+ @flickr = flickr
+
+ self.class.flickr_objects.each {|name|
+ klass = self.class.const_get name.capitalize
+ instance_variable_set "@#{name}", klass.new(@flickr)
+ }
+ end
+
+ def self.build_request(req) # :nodoc:
+ method_nesting = req.split '.'
+ raise "'#{@name}' : Method name mismatch" if method_nesting.shift != request_name.split('.').last
+
+ if method_nesting.size > 1
+ name = method_nesting.first
+ class_name = name.capitalize
+ if const_defined? class_name
+ klass = const_get( class_name)
+ else
+ klass = Class.new Request
+ const_set(class_name, klass)
+ attr_reader name
+ flickr_objects << name
+ end
+
+ klass.build_request method_nesting.join('.')
+ else
+ req = method_nesting.first
+ define_method(req) { |*args|
+ class_req = self.class.request_name
+ @flickr.call class_req + '.' + req, *args
+ }
+ flickr_methods << req
+ end
+ end
+
+ # List of the flickr subobjects of this object
+ def self.flickr_objects; @flickr_objects ||= [] end
+
+ # List of the flickr methods of this object
+ def self.flickr_methods; @flickr_methods ||= [] end
+
+ # Returns the prefix of the request corresponding to this class.
+ def self.request_name; name.downcase.gsub(/::/, '.').sub(/[^\.]+\./, '') end
+ end
+
+ # Root class of the flickr api hierarchy.
+ class Flickr < Request
+ def initialize # :nodoc:
+ super self
+ @token = nil
+ end
+
+ # This is the central method. It does the actual request to the flickr server.
+ #
+ # Raises FailedResponse if the response status is _failed_.
+ def call(req, args={})
+ path = REST_PATH + build_args(args, req).collect { |a, v| "#{a}=#{v}" }.join('&')
+ http_response = Net::HTTP.start(FLICKR_HOST) { |http| http.get(path, 'User-Agent' => "Flickraw/#{VERSION}") }
+ parse_response(http_response, req)
+ end
+
+ # Use this to upload the photo in _file_.
+ #
+ # flickr.upload_photo '/path/to/the/photo', :title => 'Title', :description => 'This is the description'
+ #
+ # See http://www.flickr.com/services/api/upload.api.html for more information on the arguments.
+ def upload_photo(file, args={})
+ photo = File.open(file, 'rb') { |f| f.read }
+ boundary = MD5.md5(photo).to_s
+
+ header = {'Content-type' => "multipart/form-data, boundary=#{boundary} ", 'User-Agent' => "Flickraw/#{VERSION}"}
+ query = ''
+ build_args(args).each { |a, v|
+ query <<
+ "--#{boundary}\r\n" <<
+ "Content-Disposition: form-data; name=\"#{a}\"\r\n\r\n" <<
+ "#{v}\r\n"
+ }
+ query <<
+ "--#{boundary}\r\n" <<
+ "Content-Disposition: form-data; name=\"photo\"; filename=\"#{file}\"\r\n" <<
+ "Content-Transfer-Encoding: binary\r\n" <<
+ "Content-Type: image/jpeg\r\n\r\n" <<
+ photo <<
+ "\r\n" <<
+ "--#{boundary}--"
+
+ http_response = Net::HTTP.start(FLICKR_HOST) { |http| http.post(UPLOAD_PATH, query, header) }
+ xml = http_response.body
+ if xml[/stat="(\w+)"/, 1] == 'fail'
+ msg = xml[/msg="([^"]+)"/, 1]
+ code = xml[/code="([^"]+)"/, 1]
+ raise FailedResponse.new(msg, code, 'flickr.upload')
+ end
+ Response.structify( {:stat => 'ok', :photoid => xml[/<photoid>(\w+)<\/photoid>/, 1], :ticketid => xml[/<ticketid>([^<]+)<\/ticketid>/, 1]})
+ end
+
+ private
+ def parse_response(response, req = nil)
+ json = Crack::JSON.parse(response.body)
+ raise FailedResponse.new(json['message'], json['code'], req) if json.delete('stat') == 'fail'
+ name, json = json.to_a.first if json.size == 1
+
+ res = Response.structify json, name
+ lookup_token(req, res)
+ res
+ end
+
+ def build_args(args={}, req = nil)
+ full_args = {:api_key => FlickRaw.api_key, :format => 'json', :nojsoncallback => 1}
+ full_args[:method] = req if req
+ full_args[:auth_token] = @token if @token
+ args.each {|k, v| full_args[k.to_sym] = v.to_s }
+ full_args[:api_sig] = FlickRaw.api_sig(full_args) if FlickRaw.shared_secret
+ args.each {|k, v| full_args[k.to_sym] = CGI.escape(v.to_s) } if req
+ full_args
+ end
+
+ def lookup_token(req, res)
+ token_reqs = ['flickr.auth.getToken', 'flickr.auth.getFullToken', 'flickr.auth.checkToken']
+ @token = res.token if token_reqs.include?(req) and res.respond_to?(:token)
+ end
+ end
+
+ class << self
+ # Your flickr API key, see http://www.flickr.com/services/api/keys for more information
+ attr_accessor :api_key
+
+ # The shared secret of _api_key_, see http://www.flickr.com/services/api/keys for more information
+ attr_accessor :shared_secret
+
+ # Returns the flickr auth URL.
+ def auth_url(args={})
+ full_args = {:api_key => FlickRaw.api_key, :perms => 'read'}
+ args.each {|k, v| full_args[k.to_sym] = v }
+ full_args[:api_sig] = api_sig(full_args) if FlickRaw.shared_secret
+
+ 'http://' + FLICKR_HOST + AUTH_PATH + full_args.collect { |a, v| "#{a}=#{v}" }.join('&')
+ end
+
+ # Returns the signature of hsh. This is meant to be passed in the _api_sig_ parameter.
+ def api_sig(hsh)
+ MD5.md5(FlickRaw.shared_secret + hsh.sort{|a, b| a[0].to_s <=> b[0].to_s }.flatten.join).to_s
+ end
+ end
+
+ def self.init_methods
+ methods = Flickr.new.call 'flickr.reflection.getMethods'
+ methods.each { |method| Flickr.build_request method }
+ end
+end
+
+def flickr; $flickraw ||= (FlickRaw.init_methods && FlickRaw::Flickr.new) end
+
+# Use this to access the flickr API easily. You can type directly the flickr requests as they are described on the flickr website.
+# require 'flickraw'
+#
+# recent_photos = flickr.photos.getRecent
+# puts recent_photos[0].title
View
122 vendor/utils/twitter_search.rb
@@ -0,0 +1,122 @@
+require 'net/http'
+require File.dirname(__FILE__) + "/crack-json.rb" # # require 'rubygems'; require 'json'
+require 'cgi'
+require 'time'
+
+module TwitterSearch
+
+ class Tweet
+ VARS = [:text, :from_user, :to_user, :to_user_id, :id, :iso_language_code, :from_user_id, :created_at, :profile_image_url, :source ]
+ attr_reader *VARS
+ attr_reader :language
+
+ def initialize(opts)
+ @language = opts['iso_language_code']
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
+ end
+
+ def time_ago
+ time_ago_or_time_stamp Time.parse( @created_at[0..18] )
+ end
+ def time_ago_or_time_stamp(from_time, to_time = Time.now, include_seconds = true, detail = false)
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
+ distance_in_seconds = ((to_time - from_time).abs).round
+ case distance_in_minutes
+ when 0..1 then time = (distance_in_seconds < 60) ? "#{distance_in_seconds} seconds ago" : '1 minute ago'
+ when 2..59 then time = "#{distance_in_minutes} minutes ago"
+ when 60..90 then time = "1 hour ago"
+ when 90..1440 then time = "#{(distance_in_minutes.to_f / 60.0).round} hours ago"
+ when 1440..2160 then time = '1 day ago' # 1-1.5 days
+ when 2160..2880 then time = "#{(distance_in_minutes.to_f / 1440.0).round} days ago" # 1.5-2 days
+ else time = from_time.strftime("%a, %d %b %Y")
+ end
+ return time_stamp(from_time) if (detail && distance_in_minutes > 2880)
+ return time
+ end
+ end
+
+ class Tweets
+ VARS = [:since_id, :max_id, :results_per_page, :page, :query, :next_page]
+ attr_reader *VARS
+
+ include Enumerable
+
+ def initialize(opts)
+ @results = opts['results'].collect { |each| Tweet.new(each) }
+ VARS.each { |each| instance_variable_set "@#{each}", opts[each.to_s] }
+ end
+
+ def each(&block)
+ @results.each(&block)
+ end
+
+ def size
+ @results.size
+ end
+
+ def [](index)
+ @results[index]
+ end
+
+ def has_next_page?
+ ! @next_page.nil?
+ end
+
+ def get_next_page
+ client = Client.new
+ return client.query( CGI.parse( @next_page[1..-1] ) )
+ end
+ end
+
+ class Client
+ TWITTER_API_URL = 'http://search.twitter.com/search.json'
+ TWITTER_API_DEFAULT_TIMEOUT = 5
+
+ attr_accessor :agent
+ attr_accessor :timeout
+
+ def initialize(agent = 'twitter-search', timeout = TWITTER_API_DEFAULT_TIMEOUT)
+ @agent = agent
+ @timeout = timeout
+ end
+
+ def headers
+ { "Content-Type" => 'application/json',
+ "User-Agent" => @agent }
+ end
+
+ def query(opts = {})
+ url = URI.parse(TWITTER_API_URL)
+ url.query = sanitize_query(opts)
+
+ req = Net::HTTP::Get.new(url.path)
+ http = Net::HTTP.new(url.host, url.port)
+ http.read_timeout = timeout
+
+ json = http.start { |http|
+ http.get("#{url.path}?#{url.query}", headers)
+ }.body
+ Tweets.new Crack::JSON.parse(json)
+ end
+
+ private
+
+ def sanitize_query(opts)
+ if opts.is_a? String
+ "q=#{CGI.escape(opts)}"
+ elsif opts.is_a? Hash
+ "#{sanitize_query_hash(opts)}"
+ end
+ end
+
+ def sanitize_query_hash(query_hash)
+ query_hash.collect { |key, value|
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
+ }.join('&')
+ end
+
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.