murlsh 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.textile ADDED
@@ -0,0 +1,32 @@
1
+ Simple site for a small group of people to share or archive urls.
2
+
3
+ * looks up url titles
4
+ * adds thumbnails for and jGrowls embedded versions of Flickr, Imageshack, Vimeo and YouTube urls
5
+ * embeds Flash mp3 player for mp3 urls
6
+ * Gravatar support
7
+ * Atom feed
8
+ * regex search
9
+ * looks good on iPhone
10
+ * rack interface
11
+ * plug-in interface
12
+
13
+ See "http://urls.matthewm.boedicker.org/":http://urls.matthewm.boedicker.org/ for example.
14
+
15
+ Phusion Passenger Setup:
16
+
17
+ <pre>
18
+ <code>
19
+ rake gemspec build
20
+ gem install pkg/murlsh-x.x.x.gem
21
+ </code>
22
+ </pre>
23
+
24
+ In the web directory:
25
+
26
+ <pre>
27
+ <code>
28
+ murlsh
29
+ edit config.yaml
30
+ rake db:init user:add
31
+ </code>
32
+ </pre>
data/Rakefile ADDED
@@ -0,0 +1,192 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'rubygems'
4
+
5
+ require 'murlsh'
6
+
7
+ require 'flog'
8
+ require 'rake/testtask'
9
+ require 'sqlite3'
10
+
11
+ require 'pp'
12
+ require 'yaml'
13
+
14
+ config = YAML.load_file('config.yaml')
15
+
16
+ desc "Test remote content type fetch for a URL and show errors."
17
+ task :content_type, :url do |t, args|
18
+ puts Murlsh.get_content_type(args.url, :failproof => false)
19
+ end
20
+
21
+ namespace :db do
22
+
23
+ desc 'Delete the last url added.'
24
+ task :delete_last_url do
25
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
26
+ :database => config.fetch('db_file'))
27
+
28
+ last = Murlsh::Url.find(:last, :order => 'time')
29
+ pp last
30
+ response = ask('Delete this url', '?')
31
+ last.destroy if %w{y yes}.include?(response.downcase)
32
+ end
33
+
34
+ desc "Check for duplicate URLs."
35
+ task :dupcheck do
36
+ db = SQLite3::Database.new(config.fetch('db_file'))
37
+ db.results_as_hash = true
38
+ h = {}
39
+ db.execute("SELECT * FROM urls").each do |r|
40
+ h[r['url']] = h.fetch(r['url'], []).push([r['id'], r['time']])
41
+ end
42
+ h.select { |k,v| v.size > 1 }.each do |k,v|
43
+ puts k
44
+ v.each { |id,time| puts " #{id} #{time}" }
45
+ end
46
+ end
47
+
48
+ desc "Create an empty database."
49
+ task :init do
50
+ puts "creating #{config.fetch('db_file')}"
51
+ db = SQLite3::Database.new(config.fetch('db_file'))
52
+ db.execute("CREATE TABLE urls (
53
+ id INTEGER PRIMARY KEY,
54
+ time TIMESTAMP,
55
+ url TEXT,
56
+ email TEXT,
57
+ name TEXT,
58
+ title TEXT,
59
+ content_type TEXT);
60
+ ")
61
+ end
62
+
63
+ desc 'Interact with the database.'
64
+ task :shell do
65
+ exec "sqlite3 #{config['db_file']}"
66
+ end
67
+
68
+ end
69
+
70
+ namespace :dreamhost do
71
+
72
+ desc "Restart Passenger."
73
+ task :restart do
74
+ open('tmp/restart.txt', 'w') { |f| }
75
+ end
76
+
77
+ end
78
+
79
+ desc "Run flog on ruby and report on complexity."
80
+ task :flog do
81
+ flog = Flog.new
82
+ flog.flog('lib')
83
+ flog.report
84
+ end
85
+
86
+ desc "Run test suite."
87
+ Rake::TestTask.new do |t|
88
+ t.pattern = 'test/*_test.rb'
89
+ t.verbose = true
90
+ t.warning = true
91
+ end
92
+
93
+ desc "Test remote title fetch for a URL and show errors."
94
+ task :title, :url do |t, args|
95
+ puts Murlsh.get_title(args.url, :failproof => false)
96
+ end
97
+
98
+ desc 'Try to fetch the title for a url and update it in the database.'
99
+ task :title_fetch, :url_id do |t, args|
100
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
101
+ :database => config.fetch('db_file'))
102
+ url = Murlsh::Url.find(args.url_id)
103
+ puts "Url: #{url.url}"
104
+ puts "Previous title: #{url.title}"
105
+ url.title = Murlsh.get_title(url.url, :failproof => false)
106
+ url.save
107
+ puts "\nNew title: #{url.title}"
108
+ end
109
+
110
+ namespace :user do
111
+
112
+ desc "Add a new user."
113
+ task :add do
114
+ puts "adding to #{config.fetch('auth_file')}"
115
+ username = ask(:username)
116
+ email = ask(:email)
117
+ password = ask(:password)
118
+
119
+ Murlsh::Auth.new(config.fetch('auth_file')).add_user(username, email,
120
+ password)
121
+ end
122
+
123
+ end
124
+
125
+ desc "Validate XHTML."
126
+ task :validate do
127
+ require 'cgi'
128
+ require 'net/http'
129
+
130
+ net_http = Net::HTTP.new('validator.w3.org', 80)
131
+ #net_http.set_debug_output(STDOUT)
132
+
133
+ check_url = config.fetch('root_url')
134
+
135
+ print "validating #{check_url} : "
136
+
137
+ net_http.start do |http|
138
+ resp = http.request_head(
139
+ "/check?uri=#{CGI::escape(check_url)}&charset=(detect+automatically)&doctype=Inline&group=0")
140
+ result = resp['X-W3C-Validator-Status']
141
+ errors = resp['X-W3C-Validator-Errors']
142
+ warnings = resp['X-W3C-Validator-Warnings']
143
+
144
+ puts "#{result} (#{errors} errors, #{warnings} warnings)"
145
+ end
146
+
147
+ end
148
+
149
+ desc 'Generate a shell script that will post a new url.'
150
+ task :post_sh do
151
+ puts <<EOS
152
+ #!/bin/sh
153
+
154
+ URL="$1"
155
+ AUTH="$2" # password can be passed as second parameter or hardcoded here
156
+
157
+ curl \\
158
+ --data-urlencode "url=${URL}" \\
159
+ --data-urlencode "auth=${AUTH}" \\
160
+ #{config.fetch('root_url')}
161
+ EOS
162
+ end
163
+
164
+ def ask(prompt, sep=':')
165
+ print "#{prompt}#{sep} "
166
+ return STDIN.gets.chomp
167
+ end
168
+
169
+ begin
170
+ require 'jeweler'
171
+ Jeweler::Tasks.new do |gemspec|
172
+ gemspec.name = 'murlsh'
173
+ gemspec.summary = 'url sharing site framework'
174
+ gemspec.description = 'url sharing site framework with easy adding, title lookup, atom feed, thumbnails and embedding'
175
+ gemspec.email = 'matthewm@boedicker.org'
176
+ gemspec.homepage = 'http://github.com/mmb/murlsh'
177
+ gemspec.authors = ['Matthew M. Boedicker']
178
+
179
+ %w{
180
+ activerecord 2.3.4
181
+ bcrypt 2.1.2
182
+ builder 2.1.2
183
+ hpricot 0.8.1
184
+ htmlentities 4.2.0
185
+ rack 1.0.1
186
+ sqlite3 1.2.5
187
+ }.each_slice(2) { |g,v| gemspec.add_dependency(g, ">= #{v}") }
188
+
189
+ end
190
+ rescue LoadError
191
+ puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
192
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
data/bin/murlsh ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+
5
+ FileUtils.cp_r(
6
+ %w{.htaccess config.ru config.yaml plugins/ public/ Rakefile}.collect { |x|
7
+ File.join(File.dirname(__FILE__), '..', x) }, '.', :verbose => true)
8
+
9
+ FileUtils.mkdir('tmp')
10
+
11
+ puts <<eos
12
+ Next steps:
13
+
14
+ edit config.yaml
15
+ rake db:init user:add
16
+ eos
data/config.ru ADDED
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'murlsh'
4
+
5
+ # use Rack::ShowExceptions
6
+ use Rack::ConditionalGet
7
+ use Rack::Deflater
8
+ use Rack::Static, :urls => %w{/css /js /swf}, :root => 'public'
9
+ use Rack::Static, :urls => %w{/atom.xml}
10
+
11
+ run Murlsh::Dispatch.new
data/config.yaml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ auth_file: /home/mmb/src/murlsh/murlsh_users
3
+ description: URLs found interesting by Matthew M. Boedicker
4
+ db_file: murlsh.db
5
+ feed_file: atom.xml
6
+ google_verify:
7
+ gravatar_size: 32
8
+ num_posts_feed: 25
9
+ num_posts_page: 100
10
+ page_title: mmb url share
11
+ root_url: http://urls.matthewm.boedicker.org/
12
+ css_prefix: css/
13
+ img_prefix: img/
14
+ js_prefix: js/
15
+ swf_prefix: swf/
16
+ css_files:
17
+ - jquery.jgrowl.css
18
+ - screen.css
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'builder'
3
+
4
+ require 'uri'
5
+
6
+ module Murlsh
7
+
8
+ class AtomFeed
9
+
10
+ def initialize(root_url, options={})
11
+ options = {
12
+ :filename => 'atom.xml',
13
+ :title => 'Atom feed' }.merge(options)
14
+ @root_url = root_url
15
+ @filename = options[:filename]
16
+ @title = options[:title]
17
+
18
+ setup_id_fields
19
+ end
20
+
21
+ def setup_id_fields
22
+ uri_parsed = URI(@root_url)
23
+
24
+ m = uri_parsed.host.match(/^(.*?)\.?([^.]+\.[^.]+)$/)
25
+
26
+ @host, @domain = (m ? m.captures : [uri_parsed.host, ''])
27
+
28
+ @path = uri_parsed.path
29
+ end
30
+
31
+ def write(entries, path)
32
+ open(path, 'w') do |f|
33
+ f.flock(File::LOCK_EX)
34
+
35
+ make(entries, :target => f)
36
+
37
+ f.flock(File::LOCK_UN)
38
+ end
39
+ end
40
+
41
+ def make(entries, options={})
42
+ xm = Builder::XmlMarkup.new(options)
43
+ xm.instruct! :xml
44
+
45
+ xm.feed(:xmlns => 'http://www.w3.org/2005/Atom') {
46
+ xm.id(@root_url)
47
+ xm.link(:href => URI.join(@root_url, @filename), :rel => 'self')
48
+ xm.title(@title)
49
+ xm.updated(entries.collect { |mu| mu.time }.max.xmlschema)
50
+ entries.each do |mu|
51
+ xm.entry {
52
+ xm.author { xm.name(mu.name) }
53
+ xm.title(mu.title)
54
+ xm.id(entry_id(mu))
55
+ xm.summary(mu.title)
56
+ xm.updated(mu.time.xmlschema)
57
+ xm.link(:href => mu.url)
58
+ enclosure(xm, mu)
59
+ }
60
+ end
61
+ }
62
+ xm
63
+ end
64
+
65
+ def entry_id(url)
66
+ "tag:#{@domain},#{url.time.strftime('%Y-%m-%d')}:#{@host}#{@path}#{url.id}"
67
+ end
68
+
69
+ def enclosure(xm, mu)
70
+ xm.link(:rel => 'enclosure', :type => mu.content_type, :href => mu.url,
71
+ :title => 'Full-size') if mu.is_image?
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'bcrypt'
3
+
4
+ require 'csv'
5
+ require 'digest/md5'
6
+
7
+ module Murlsh
8
+
9
+ class Auth
10
+
11
+ def initialize(file)
12
+ @file = file
13
+ end
14
+
15
+ def auth(password)
16
+ CSV::Reader.parse(open(@file)) do |row|
17
+ return { :name => row[0], :email => row[1] } if
18
+ BCrypt::Password.new(row[2]) == password
19
+ end
20
+ end
21
+
22
+ def add_user(username, email, password)
23
+ open(@file, 'a') do |f|
24
+ f.flock(File::LOCK_EX)
25
+ f.write("#{[username, Digest::MD5.hexdigest(email),
26
+ BCrypt::Password.create(password)].join(',')}\n")
27
+ f.flock(File::LOCK_UN)
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,54 @@
1
+ %w{
2
+ murlsh
3
+
4
+ rubygems
5
+ active_record
6
+ rack
7
+ sqlite3
8
+
9
+ yaml
10
+ }.each { |m| require m }
11
+
12
+ module Murlsh
13
+
14
+ class Dispatch
15
+
16
+ def initialize
17
+ @config = YAML.load_file('config.yaml')
18
+ @url_root = URI(@config.fetch('root_url')).path
19
+
20
+ ActiveRecord::Base.establish_connection(
21
+ :adapter => 'sqlite3', :database => @config.fetch('db_file'))
22
+
23
+ @db = ActiveRecord::Base.connection.instance_variable_get(:@connection)
24
+
25
+ @url_server = Murlsh::UrlServer.new(@config, @db)
26
+ end
27
+
28
+ def call(env)
29
+ dispatch = {
30
+ ['GET', @url_root] => [@url_server, :get],
31
+ ['POST', @url_root] => [@url_server, :post],
32
+ ['GET', "#{@url_root}url"] => [@url_server, :get],
33
+ ['POST', "#{@url_root}url"] => [@url_server, :post],
34
+ }
35
+ dispatch.default = [self, :not_found]
36
+
37
+ req = Rack::Request.new(env)
38
+
39
+ obj, meth = dispatch[[req.request_method, req.path]]
40
+
41
+ obj.send(meth, req).finish
42
+ end
43
+
44
+ def not_found(req)
45
+ Rack::Response.new("<p>#{req.url} not found</p>
46
+
47
+ <p><a href=\"#{@config['root_url']}\">root<a></p>
48
+ ",
49
+ 404, { 'Content-Type' => 'text/html' })
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,84 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+
5
+ class URI::Generic
6
+
7
+ def path_query
8
+ path + (query ? "?#{query}" : '')
9
+ end
10
+
11
+ end
12
+
13
+ module Murlsh
14
+
15
+ module_function
16
+
17
+ def get_content_type(url, options={})
18
+ options[:headers] = default_headers(url).merge(
19
+ options.fetch(:headers, {}))
20
+
21
+ options = {
22
+ :failproof => true,
23
+ :redirects => 0,
24
+ }.merge(options)
25
+
26
+ unless options[:redirects] > 3
27
+ begin
28
+ url = parse_uri(url)
29
+
30
+ make_net_http(url, options).start do |http|
31
+ resp = get_resp(http, url, options[:headers])
32
+ case resp
33
+ when Net::HTTPSuccess then return resp['content-type']
34
+ when Net::HTTPRedirection then
35
+ options[:redirects] += 1
36
+ return get_content_type(resp['location'], options)
37
+ end
38
+ end
39
+ rescue Exception => e
40
+ raise unless options[:failproof]
41
+ end
42
+ end
43
+ ''
44
+ end
45
+
46
+ # Parse a URI if it's not already parsed.
47
+ def parse_uri(uri)
48
+ uri.is_a?(URI::HTTP) ? uri : URI(uri)
49
+ end
50
+
51
+ def make_net_http(url, options={})
52
+ net_http = Net::HTTP.new(url.host, url.port)
53
+ net_http.use_ssl = (url.scheme == 'https')
54
+ net_http.set_debug_output(options[:debug]) if options[:debug]
55
+ net_http
56
+ end
57
+
58
+ # Get the response to HTTP HEAD. If HEAD not allowed do GET.
59
+ def get_resp(http, url, headers={})
60
+ resp = http.request_head(url.path_query, headers)
61
+ if Net::HTTPMethodNotAllowed === resp
62
+ http.request_get(url.path_query, headers)
63
+ else
64
+ resp
65
+ end
66
+ end
67
+
68
+ def default_headers(url)
69
+ result = {
70
+ 'User-Agent' =>
71
+ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4) Gecko/20030624',
72
+ }
73
+ begin
74
+ parsed_url = parse_uri(url)
75
+ if (parsed_url.host || '')[/^www\.nytimes\.com/]
76
+ result['Referer'] = 'http://news.google.com/'
77
+ end
78
+ rescue URI::InvalidURIError => e
79
+ end
80
+
81
+ result
82
+ end
83
+
84
+ end
@@ -0,0 +1,65 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'htmlentities'
4
+
5
+ require 'iconv'
6
+ require 'open-uri'
7
+ require 'uri'
8
+
9
+ module Murlsh
10
+
11
+ module_function
12
+
13
+ def get_title(url, options={})
14
+ options[:headers] = default_headers(url).merge(
15
+ options.fetch(:headers, {}))
16
+
17
+ options = {
18
+ :failproof => true,
19
+ }.merge(options)
20
+
21
+ result = nil
22
+ begin
23
+ options[:content_type] ||= get_content_type(url, options)
24
+ if might_have_title(options[:content_type])
25
+ f = open(url, options[:headers])
26
+
27
+ doc = Hpricot(f)
28
+
29
+ result = HTMLEntities.new.decode(Iconv.conv('utf-8',
30
+ get_charset(doc) || f.charset, find_title(doc)))
31
+ end
32
+ rescue Exception => e
33
+ raise unless options[:failproof]
34
+ end
35
+ (result and !result.empty?) ? result : url
36
+ end
37
+
38
+ def might_have_title(content_type)
39
+ content_type[/^text\/html/]
40
+ end
41
+
42
+ # Find the title in an Hpricot document.
43
+ def find_title(doc)
44
+ %w{//html/head/title //head/title //html/title //title}.each do |xpath|
45
+ return (doc/xpath).first.inner_html unless (doc/xpath).first.nil?
46
+ end
47
+ nil
48
+ end
49
+
50
+ # Get the character set of an Hpricot document.
51
+ def get_charset(doc)
52
+ %w{content-type Content-Type}.each do |ct|
53
+ content_type = doc.at("meta[@http-equiv='#{ct}']")
54
+ unless content_type.nil?
55
+ content = content_type['content']
56
+ unless content.nil?
57
+ charset = content[/charset=([\w_.:-]+)/, 1]
58
+ return charset if charset
59
+ end
60
+ end
61
+ end
62
+ nil
63
+ end
64
+
65
+ end
@@ -0,0 +1,97 @@
1
+ module Murlsh
2
+
3
+ module Markup
4
+
5
+ def javascript(sources, options={})
6
+ sources.to_a.each do |src|
7
+ script('', :type => 'text/javascript',
8
+ :src => "#{options[:prefix]}#{src}")
9
+ end
10
+ end
11
+
12
+ def murlsh_img(options={})
13
+ img_convert_prefix(options)
14
+ img_convert_size(options)
15
+ img_convert_text(options)
16
+
17
+ if options[:href]
18
+ a(:href => options[:href]) {
19
+ options.delete(:href)
20
+ img(options)
21
+ }
22
+ else
23
+ img(options)
24
+ end
25
+ end
26
+
27
+ def atom(href)
28
+ link(:rel => 'alternate', :type => 'application/atom+xml', :href => href)
29
+ end
30
+
31
+ def css(hrefs, options={})
32
+ hrefs.to_a.each do |href|
33
+ attrs = {
34
+ :href => "#{options[:prefix]}#{href}",
35
+ :rel => 'stylesheet',
36
+ :type => 'text/css',
37
+ }
38
+ attrs[:media] = options[:media] if options[:media]
39
+ link(attrs)
40
+ end
41
+ end
42
+
43
+ def metas(tags)
44
+ tags.each { |k,v| meta(:name => k, :content => v) }
45
+ end
46
+
47
+ def gravatar(email_hash, options={})
48
+ query = options.reject do |k,v|
49
+ not ((k == 'd' and %w{identicon monsterid wavatar}.include?(v)) or
50
+ (k =='s' and (0..512).include?(v)) or
51
+ (k == 'r' and %w{g pg r x}.include?(v)))
52
+ end
53
+
54
+ return if query['s'] and query['s'] < 1
55
+
56
+ options.reject! { |k,v| %w{d s r}.include?(k) }
57
+ options[:src] = URI.join('http://www.gravatar.com/avatar/',
58
+ email_hash + build_query(query))
59
+
60
+ murlsh_img(options)
61
+ end
62
+
63
+ def build_query(h)
64
+ h.empty? ? '' :
65
+ '?' + h.collect { |k,v| URI.escape("#{k}=#{v}") }.join('&')
66
+ end
67
+
68
+ private
69
+
70
+ def img_convert_prefix(options)
71
+ if options.has_key?(:prefix) and options.has_key?(:src)
72
+ options[:src] = options[:prefix] + options[:src]
73
+ options.delete(:prefix)
74
+ end
75
+ end
76
+
77
+ def img_convert_size(options)
78
+ if options.has_key?(:size)
79
+ if options[:size].kind_of?(Array) and options[:size].size == 2
80
+ options[:width], options[:height] = options[:size]
81
+ else
82
+ options[:width] = options[:height] = options[:size]
83
+ end
84
+ options.delete(:size)
85
+ end
86
+ end
87
+
88
+ def img_convert_text(options)
89
+ if options.has_key?(:text)
90
+ options[:alt] = options[:title] = options[:text]
91
+ options.delete(:text)
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end