├── .gitignore
├── .idea
└── scopes
│ └── scope_settings.xml
├── Gemfile
├── README.md
├── Rakefile
├── install.rb
├── lib
├── configtools.rb
├── dayone.rb
├── feed-normalizer
│ ├── feed-normalizer.rb
│ ├── html-cleaner.rb
│ ├── parsers
│ │ ├── rss.rb
│ │ └── simple-rss.rb
│ └── structures.rb
├── html2text
├── levenshtein-0.2.2
│ ├── CHANGELOG
│ ├── LICENSE
│ ├── README
│ ├── VERSION
│ ├── ext
│ │ └── levenshtein
│ │ │ ├── .RUBYARCHDIR.time
│ │ │ ├── Makefile
│ │ │ ├── extconf.rb
│ │ │ ├── levenshtein.h
│ │ │ ├── levenshtein_fast.bundle
│ │ │ ├── levenshtein_fast.c
│ │ │ └── mkmf.log
│ └── lib
│ │ ├── levenshtein.rb
│ │ └── levenshtein
│ │ ├── levenshtein_fast.bundle
│ │ └── version.rb
├── multimarkdown
├── plist.rb
├── redirect.rb
└── sociallogger.rb
├── plugin_template.rb
├── plugins
├── BlogLogger.rb
├── appnetlogger.rb
├── flickrlogger.rb
├── foursquarelogger.rb
├── githublogger.rb
├── goodreadslogger.rb
├── instapaperlogger.rb
├── lastfmlogger.rb
├── pinboardlogger.rb
├── pocketlogger.rb
├── rsslogger.rb
└── twitterlogger.rb
├── plugins_disabled
├── asanalogger.rb
├── facebookifttt.rb
├── feedafever.rb
├── feedlogger.rb
├── fitbit.rb
├── flickrlogger_rss.rb
├── gaugeslogger.rb
├── getgluelogger.rb
├── gistlogger.rb
├── githubcommitlogger.rb
├── googleanalyticslogger.rb
├── lastfmcovers.rb
├── misologger.rb
├── movesapplogger.rb
├── olivetree.rb
├── omnifocus.rb
├── pocketlogger_api.rb
├── rdiologger.rb
├── readability_api.rb
├── reporterlogger.rb
├── runkeeper.rb
├── soundcloudlogger.rb
├── stravalogger.rb
├── thehitlist.rb
├── things.rb
├── timingapplogger.rb
├── todoist.rb
├── traktlogger.rb
├── untappd.rb
├── wunderlistlogger.rb
└── yahoofinancelogger.rb
├── rssfeedlist.md
├── slogger
├── slogger.develop.rb
├── slogger.rb
├── slogger_image.rb
├── spec
└── plugins
│ ├── fixtures
│ └── strava.yml
│ ├── mock_day_one.rb
│ ├── mock_slogger.rb
│ ├── spec_helper.rb
│ └── stravalogger_spec.rb
└── test
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sublime*
2 | *.bak
3 | *.taskpaper
4 | *.develop
5 | .DS_Store
6 | *_config
7 | plugins_develop
8 | runlog.txt
9 | /slogger.log
10 | *test.*
11 | projectnotes.md
12 | bintest
13 | .fuse*
14 | Gemfile.lock
15 | .bundle
16 | vendor/bundle
17 |
--------------------------------------------------------------------------------
/.idea/scopes/scope_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'feed-normalizer'
4 | gem 'twitter', '~> 5.3.0'
5 | gem 'twitter_oauth'
6 | gem 'json'
7 | gem 'sinatra'
8 |
9 | gem 'nokogiri'
10 | gem 'digest' # required for feedafever
11 | gem 'sqlite3' # required for feedafever
12 | gem 'rmagick', '2.13.2' # required for lastfmcovers
13 |
14 | group :test do
15 | gem 'rake'
16 | gem 'rspec', '< 3.0'
17 | gem 'vcr'
18 | gem 'webmock'
19 | end
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rspec/core/rake_task'
2 |
3 | RSpec::Core::RakeTask.new(:spec)
4 |
5 | task :default => :spec
6 |
7 |
--------------------------------------------------------------------------------
/install.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | curloc = File.expand_path(File.dirname(__FILE__))
4 | unless File.exists?(curloc+'/slogger_config')
5 | puts
6 | puts "Please run `#{curloc}/slogger` once to generate the configuration file."
7 | puts
8 | puts "The file will show up in your slogger folder, and you can edit usernames"
9 | puts "and options in it. Once you're done, run this installer again."
10 | exit
11 | end
12 |
13 | puts
14 | puts "Installing Slogger logging scheduler"
15 | puts "This script will install the following files:"
16 | puts "~/Library/LaunchAgents/com.brettterpstra.slogger.plist"
17 | puts
18 | puts "Is '#{curloc}' the location of your Slogger folder?"
19 | print "(Y/n)"
20 | ans = gets.chomp
21 | if ans.downcase == "n"
22 | puts "Please enter the path to the 'slogger' folder on your drive"
23 | print "> "
24 | dir = gets.chomp
25 | else
26 | dir = curloc
27 | end
28 |
29 | if File.exists?(dir+"/slogger")
30 | opts = []
31 | puts "By default, Slogger runs once a day at 11:50PM."
32 | puts "If your computer is not always on, you can have"
33 | puts "Slogger fetch data back to the time of the last"
34 | puts "successful run."
35 | puts
36 | puts "Is your Mac routinely offline at 11:50PM?"
37 | print "(Y/n)"
38 | ans = gets.chomp
39 | opts.push("-s") unless ans.downcase == "n"
40 |
41 | flags = ""
42 | opts.each {|flag|
43 | flags += "\n\t\t#{flag}"
44 | }
45 |
46 | print "Setting up launchd... "
47 | xml=<
49 |
50 |
51 |
52 | Label
53 | com.brettterpstra.Slogger
54 | ProgramArguments
55 |
56 | /usr/bin/ruby
57 | #{dir}/slogger#{flags}
58 |
59 | StartCalendarInterval
60 |
61 | Hour
62 | 23
63 | Minute
64 | 50
65 |
66 |
67 |
68 | LAUNCHCTLPLIST
69 |
70 | target_dir = File.expand_path("~/Library/LaunchAgents")
71 | target_file = File.expand_path(target_dir+"/com.brettterpstra.slogger.plist")
72 |
73 | Dir.mkdir(target_dir) unless File.exists?(target_dir)
74 |
75 | open(target_file,'w') { |f|
76 | f.puts xml
77 | } unless File.exists?(target_file)
78 |
79 | %x{launchctl load "#{target_file}"}
80 | puts "done!"
81 | puts
82 | puts "----------------------"
83 | puts "Installation complete."
84 |
85 | else
86 | puts "Slogger doesn't appear to exist in the directory specified."
87 | puts "Please check your file location and try again."
88 | end
89 |
--------------------------------------------------------------------------------
/lib/configtools.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | class ConfigTools
4 | attr_accessor :config_file
5 | def initialize(options)
6 | YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE) && RUBY_VERSION < "2.0.0"
7 | @config_file = options['config_file']
8 | end
9 |
10 | def load_config
11 | File.open(@config_file) { |yf| YAML::load(yf) }
12 | end
13 |
14 | def dump_config (config)
15 | File.open(@config_file, 'w') { |yf| YAML::dump(config, yf) }
16 | end
17 |
18 | def default_config
19 | config = {
20 | 'storage' => 'icloud',
21 | 'image_filename_is_title' => true,
22 | 'date_format' => '%F',
23 | 'time_format' => '%R'
24 | }
25 | config
26 | end
27 |
28 | def config_exists?
29 | if !File.exists?(@config_file)
30 | dump_config( default_config )
31 | puts "Please update the configuration file at #{@config_file} then run Slogger again."
32 | Process.exit(-1)
33 | # return false
34 | else
35 | return true
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/feed-normalizer/feed-normalizer.rb:
--------------------------------------------------------------------------------
1 | require ENV['SLOGGER_HOME'] + '/lib/feed-normalizer/structures'
2 | require ENV['SLOGGER_HOME'] + '/lib/feed-normalizer/html-cleaner'
3 |
4 | module FeedNormalizer
5 |
6 | # The root parser object. Every parser must extend this object.
7 | class Parser
8 |
9 | # Parser being used.
10 | def self.parser
11 | nil
12 | end
13 |
14 | # Parses the given feed, and returns a normalized representation.
15 | # Returns nil if the feed could not be parsed.
16 | def self.parse(feed, loose)
17 | nil
18 | end
19 |
20 | # Returns a number to indicate parser priority.
21 | # The lower the number, the more likely the parser will be used first,
22 | # and vice-versa.
23 | def self.priority
24 | 0
25 | end
26 |
27 | protected
28 |
29 | # Some utility methods that can be used by subclasses.
30 |
31 | # sets value, or appends to an existing value
32 | def self.map_functions!(mapping, src, dest)
33 |
34 | mapping.each do |dest_function, src_functions|
35 | src_functions = [src_functions].flatten # pack into array
36 |
37 | src_functions.each do |src_function|
38 | value = if src.respond_to?(src_function)
39 | src.send(src_function)
40 | elsif src.respond_to?(:has_key?)
41 | src[src_function]
42 | end
43 |
44 | unless value.to_s.empty?
45 | append_or_set!(value, dest, dest_function)
46 | break
47 | end
48 | end
49 |
50 | end
51 | end
52 |
53 | def self.append_or_set!(value, object, object_function)
54 | if object.send(object_function).respond_to? :push
55 | object.send(object_function).push(value)
56 | else
57 | object.send(:"#{object_function}=", value)
58 | end
59 | end
60 |
61 | private
62 |
63 | # Callback that ensures that every parser gets registered.
64 | def self.inherited(subclass)
65 | ParserRegistry.register(subclass)
66 | end
67 |
68 | end
69 |
70 |
71 | # The parser registry keeps a list of current parsers that are available.
72 | class ParserRegistry
73 |
74 | @@parsers = []
75 |
76 | def self.register(parser)
77 | @@parsers << parser
78 | end
79 |
80 | # Returns a list of currently registered parsers, in order of priority.
81 | def self.parsers
82 | @@parsers.sort_by { |parser| parser.priority }
83 | end
84 |
85 | end
86 |
87 |
88 | class FeedNormalizer
89 |
90 | # Parses the given xml and attempts to return a normalized Feed object.
91 | # Setting +force_parser+ to a suitable parser will mean that parser is
92 | # used first, and if +try_others+ is false, it is the only parser used,
93 | # otherwise all parsers in the ParserRegistry are attempted, in
94 | # order of priority.
95 | #
96 | # ===Available options
97 | #
98 | # * :force_parser - instruct feed-normalizer to try the specified
99 | # parser first. Takes a class, such as RubyRssParser, or SimpleRssParser.
100 | #
101 | # * :try_others - +true+ or +false+, defaults to +true+.
102 | # If +true+, other parsers will be used as described above. The option
103 | # is useful if combined with +force_parser+ to only use a single parser.
104 | #
105 | # * :loose - +true+ or +false+, defaults to +false+.
106 | #
107 | # Specifies parsing should be done loosely. This means that when
108 | # feed-normalizer would usually throw away data in order to meet
109 | # the requirement of keeping resulting feed outputs the same regardless
110 | # of the underlying parser, the data will instead be kept. This currently
111 | # affects the following items:
112 | # * Categories: RSS allows for multiple categories per feed item.
113 | # * Limitation: SimpleRSS can only return the first category
114 | # for an item.
115 | # * Result: When loose is true, the extra categories are kept,
116 | # of course, only if the parser is not SimpleRSS.
117 | def self.parse(xml, opts = {})
118 |
119 | # Get a string ASAP, as multiple read()'s will start returning nil..
120 | xml = xml.respond_to?(:read) ? xml.read : xml.to_s
121 |
122 | if opts[:force_parser]
123 | result = opts[:force_parser].parse(xml, opts[:loose])
124 |
125 | return result if result
126 | return nil if opts[:try_others] == false
127 | end
128 |
129 | ParserRegistry.parsers.each do |parser|
130 | result = parser.parse(xml, opts[:loose])
131 | return result if result
132 | end
133 |
134 | # if we got here, no parsers worked.
135 | return nil
136 | end
137 | end
138 |
139 |
140 | parser_dir = File.dirname(__FILE__) + '/parsers'
141 |
142 | # Load up the parsers
143 | Dir.open(parser_dir).each do |fn|
144 | next unless fn =~ /[.]rb$/
145 | require "parsers/#{fn}"
146 | end
147 |
148 | end
149 |
150 |
--------------------------------------------------------------------------------
/lib/feed-normalizer/html-cleaner.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'hpricot'
3 | require 'cgi'
4 |
5 | module FeedNormalizer
6 |
7 | # Various methods for cleaning up HTML and preparing it for safe public
8 | # consumption.
9 | #
10 | # Documents used for refrence:
11 | # - http://www.w3.org/TR/html4/index/attributes.html
12 | # - http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
13 | # - http://feedparser.org/docs/html-sanitization.html
14 | # - http://code.whytheluckystiff.net/hpricot/wiki
15 | class HtmlCleaner
16 |
17 | # allowed html elements.
18 | HTML_ELEMENTS = %w(
19 | a abbr acronym address area b bdo big blockquote br button caption center
20 | cite code col colgroup dd del dfn dir div dl dt em fieldset font h1 h2 h3
21 | h4 h5 h6 hr i img ins kbd label legend li map menu ol optgroup p pre q s
22 | samp small span strike strong sub sup table tbody td tfoot th thead tr tt
23 | u ul var
24 | )
25 |
26 | # allowed attributes.
27 | HTML_ATTRS = %w(
28 | abbr accept accept-charset accesskey align alt axis border cellpadding
29 | cellspacing char charoff charset checked cite class clear cols colspan
30 | color compact coords datetime dir disabled for frame headers height href
31 | hreflang hspace id ismap label lang longdesc maxlength media method
32 | multiple name nohref noshade nowrap readonly rel rev rows rowspan rules
33 | scope selected shape size span src start summary tabindex target title
34 | type usemap valign value vspace width
35 | )
36 |
37 | # allowed attributes, but they can contain URIs, extra caution required.
38 | # NOTE: That means this doesnt list *all* URI attrs, just the ones that are allowed.
39 | HTML_URI_ATTRS = %w(
40 | href src cite usemap longdesc
41 | )
42 |
43 | DODGY_URI_SCHEMES = %w(
44 | javascript vbscript mocha livescript data
45 | )
46 |
47 | class << self
48 |
49 | # Does this:
50 | # - Unescape HTML
51 | # - Parse HTML into tree
52 | # - Find 'body' if present, and extract tree inside that tag, otherwise parse whole tree
53 | # - Each tag:
54 | # - remove tag if not whitelisted
55 | # - escape HTML tag contents
56 | # - remove all attributes not on whitelist
57 | # - extra-scrub URI attrs; see dodgy_uri?
58 | #
59 | # Extra (i.e. unmatched) ending tags and comments are removed.
60 | def clean(str)
61 | str = unescapeHTML(str)
62 |
63 | doc = Hpricot(str, :fixup_tags => true)
64 | doc = subtree(doc, :body)
65 |
66 | # get all the tags in the document
67 | # Somewhere near hpricot 0.4.92 "*" starting to return all elements,
68 | # including text nodes instead of just tagged elements.
69 | tags = (doc/"*").inject([]) { |m,e| m << e.name if(e.respond_to?(:name) && e.name =~ /^\w+$/) ; m }.uniq
70 |
71 | # Remove tags that aren't whitelisted.
72 | remove_tags!(doc, tags - HTML_ELEMENTS)
73 | remaining_tags = tags & HTML_ELEMENTS
74 |
75 | # Remove attributes that aren't on the whitelist, or are suspicious URLs.
76 | (doc/remaining_tags.join(",")).each do |element|
77 | element.raw_attributes.reject! do |attr,val|
78 | !HTML_ATTRS.include?(attr) || (HTML_URI_ATTRS.include?(attr) && dodgy_uri?(val))
79 | end
80 |
81 | element.raw_attributes = element.raw_attributes.build_hash {|a,v| [a, add_entities(v)]}
82 | end unless remaining_tags.empty?
83 |
84 | doc.traverse_text {|t| t.set(add_entities(t.to_html))}
85 |
86 | # Return the tree, without comments. Ugly way of removing comments,
87 | # but can't see a way to do this in Hpricot yet.
88 | doc.to_s.gsub(/<\!--.*?-->/mi, '')
89 | end
90 |
91 | # For all other feed elements:
92 | # - Unescape HTML.
93 | # - Parse HTML into tree (taking 'body' as root, if present)
94 | # - Takes text out of each tag, and escapes HTML.
95 | # - Returns all text concatenated.
96 | def flatten(str)
97 | str.gsub!("\n", " ")
98 | str = unescapeHTML(str)
99 |
100 | doc = Hpricot(str, :xhtml_strict => true)
101 | doc = subtree(doc, :body)
102 |
103 | out = []
104 | doc.traverse_text {|t| out << add_entities(t.to_html)}
105 |
106 | return out.join
107 | end
108 |
109 | # Returns true if the given string contains a suspicious URL,
110 | # i.e. a javascript link.
111 | #
112 | # This method rejects javascript, vbscript, livescript, mocha and data URLs.
113 | # It *could* be refined to only deny dangerous data URLs, however.
114 | def dodgy_uri?(uri)
115 | uri = uri.to_s
116 |
117 | # special case for poorly-formed entities (missing ';')
118 | # if these occur *anywhere* within the string, then throw it out.
119 | return true if (uri =~ /&\#(\d+|x[0-9a-f]+)[^;\d]/mi)
120 |
121 | # Try escaping as both HTML or URI encodings, and then trying
122 | # each scheme regexp on each
123 | [unescapeHTML(uri), CGI.unescape(uri)].each do |unesc_uri|
124 | DODGY_URI_SCHEMES.each do |scheme|
125 |
126 | regexp = "#{scheme}:".gsub(/./) do |char|
127 | "([\000-\037\177\s]*)#{char}"
128 | end
129 |
130 | # regexp looks something like
131 | # /\A([\000-\037\177\s]*)j([\000-\037\177\s]*)a([\000-\037\177\s]*)v([\000-\037\177\s]*)a([\000-\037\177\s]*)s([\000-\037\177\s]*)c([\000-\037\177\s]*)r([\000-\037\177\s]*)i([\000-\037\177\s]*)p([\000-\037\177\s]*)t([\000-\037\177\s]*):/mi
132 | return true if (unesc_uri =~ %r{\A#{regexp}}mi)
133 | end
134 | end
135 |
136 | nil
137 | end
138 |
139 | # unescapes HTML. If xml is true, also converts XML-only named entities to HTML.
140 | def unescapeHTML(str, xml = true)
141 | CGI.unescapeHTML(xml ? str.gsub("'", "'") : str)
142 | end
143 |
144 | # Adds entities where possible.
145 | # Works like CGI.escapeHTML, but will not escape existing entities;
146 | # i.e. { will NOT become {
147 | #
148 | # This method could be improved by adding a whitelist of html entities.
149 | def add_entities(str)
150 | str.to_s.gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/
183 | # Date: Fri, 11 Aug 2006 03:19:13 +0900
184 | class Hpricot::Text #:nodoc:
185 | def set(string)
186 | @content = string
187 | self.raw_string = string
188 | end
189 | end
190 |
191 |
--------------------------------------------------------------------------------
/lib/feed-normalizer/parsers/rss.rb:
--------------------------------------------------------------------------------
1 | require 'rss'
2 |
3 | # For some reason, this is only included in the RDF Item by default.
4 | class RSS::Rss::Channel::Item # :nodoc:
5 | include RSS::ContentModel
6 | end
7 |
8 | module FeedNormalizer
9 | class RubyRssParser < Parser
10 |
11 | def self.parser
12 | RSS::Parser
13 | end
14 |
15 | def self.parse(xml, loose)
16 | begin
17 | rss = parser.parse(xml)
18 | rescue Exception => e
19 | #puts "Parser #{parser} failed because #{e.message.gsub("\n",', ')}"
20 | return nil
21 | end
22 |
23 | rss ? package(rss, loose) : nil
24 | end
25 |
26 | # Fairly high priority; a fast and strict parser.
27 | def self.priority
28 | 100
29 | end
30 |
31 | protected
32 |
33 | def self.package(rss, loose)
34 | feed = Feed.new(self)
35 |
36 | # channel elements
37 | feed_mapping = {
38 | :generator => :generator,
39 | :title => :title,
40 | :urls => :link,
41 | :description => :description,
42 | :copyright => :copyright,
43 | :authors => :managingEditor,
44 | :last_updated => [:lastBuildDate, :pubDate, :dc_date],
45 | :id => :guid,
46 | :ttl => :ttl
47 | }
48 |
49 | # make two passes, to catch all possible root elements
50 | map_functions!(feed_mapping, rss, feed)
51 | map_functions!(feed_mapping, rss.channel, feed)
52 |
53 | # custom channel elements
54 | feed.image = rss.image ? rss.image.url : nil
55 | feed.skip_hours = skip(rss, :skipHours)
56 | feed.skip_days = skip(rss, :skipDays)
57 |
58 | # item elements
59 | item_mapping = {
60 | :date_published => [:pubDate, :dc_date],
61 | :urls => :link,
62 | :description => :description,
63 | :content => [:content_encoded, :description],
64 | :title => :title,
65 | :authors => [:author, :dc_creator],
66 | :last_updated => [:pubDate, :dc_date] # This is effectively an alias for date_published for this parser.
67 | }
68 |
69 | rss.items.each do |rss_item|
70 | feed_entry = Entry.new
71 | map_functions!(item_mapping, rss_item, feed_entry)
72 |
73 | # custom item elements
74 | feed_entry.id = rss_item.guid.content if rss_item.respond_to?(:guid) && rss_item.guid
75 | feed_entry.copyright = rss.copyright if rss_item.respond_to? :copyright
76 | feed_entry.categories = loose ?
77 | rss_item.categories.collect{|c|c.content} :
78 | [rss_item.categories.first.content] rescue []
79 |
80 | feed.entries << feed_entry
81 | end
82 |
83 | feed
84 | end
85 |
86 | def self.skip(parser, attribute)
87 | attributes = case attribute
88 | when :skipHours: :hours
89 | when :skipDays: :days
90 | end
91 | channel = parser.channel
92 |
93 | return nil unless channel.respond_to?(attribute) && a = channel.send(attribute)
94 | a.send(attributes).collect{|e| e.content}
95 | end
96 |
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/feed-normalizer/parsers/simple-rss.rb:
--------------------------------------------------------------------------------
1 | require 'simple-rss'
2 |
3 | # Monkey patches for outstanding issues logged in the simple-rss project.
4 | # * Add support for issued time field:
5 | # http://rubyforge.org/tracker/index.php?func=detail&aid=13980&group_id=893&atid=3517
6 | # * The '+' symbol is lost when escaping fields.
7 | # http://rubyforge.org/tracker/index.php?func=detail&aid=10852&group_id=893&atid=3517
8 | #
9 | class SimpleRSS
10 | @@item_tags << :issued
11 |
12 | undef clean_content
13 | def clean_content(tag, attrs, content)
14 | content = content.to_s
15 | case tag
16 | when :pubDate, :lastBuildDate, :published, :updated, :expirationDate, :modified, :'dc:date', :issued
17 | Time.parse(content) rescue unescape(content)
18 | when :author, :contributor, :skipHours, :skipDays
19 | unescape(content.gsub(/<.*?>/,''))
20 | else
21 | content.empty? && "#{attrs} " =~ /href=['"]?([^'"]*)['" ]/mi ? $1.strip : unescape(content)
22 | end
23 | end
24 |
25 | undef unescape
26 | def unescape(s)
27 | if s =~ /^()/
28 | # Raw HTML is inside the CDATA, so just remove the CDATA wrapper.
29 | s.gsub(/()/,'').strip
30 | elsif s =~ /[<>]/
31 | # Already looks like HTML.
32 | s
33 | else
34 | # Make it HTML.
35 | FeedNormalizer::HtmlCleaner.unescapeHTML(s)
36 | end
37 | end
38 | end
39 |
40 | module FeedNormalizer
41 |
42 | # The SimpleRSS parser can handle both RSS and Atom feeds.
43 | class SimpleRssParser < Parser
44 |
45 | def self.parser
46 | SimpleRSS
47 | end
48 |
49 | def self.parse(xml, loose)
50 | begin
51 | atomrss = parser.parse(xml)
52 | rescue Exception => e
53 | #puts "Parser #{parser} failed because #{e.message.gsub("\n",', ')}"
54 | return nil
55 | end
56 |
57 | package(atomrss)
58 | end
59 |
60 | # Fairly low priority; a slower, liberal parser.
61 | def self.priority
62 | 900
63 | end
64 |
65 | protected
66 |
67 | def self.package(atomrss)
68 | feed = Feed.new(self)
69 |
70 | # root elements
71 | feed_mapping = {
72 | :generator => :generator,
73 | :title => :title,
74 | :last_updated => [:updated, :lastBuildDate, :pubDate, :dc_date],
75 | :copyright => [:copyright, :rights],
76 | :authors => [:author, :webMaster, :managingEditor, :contributor],
77 | :urls => :link,
78 | :description => [:description, :subtitle],
79 | :ttl => :ttl
80 | }
81 |
82 | map_functions!(feed_mapping, atomrss, feed)
83 |
84 | # custom channel elements
85 | feed.id = feed_id(atomrss)
86 | feed.image = image(atomrss)
87 |
88 |
89 | # entry elements
90 | entry_mapping = {
91 | :date_published => [:pubDate, :published, :dc_date, :issued],
92 | :urls => :link,
93 | :description => [:description, :summary],
94 | :content => [:content, :content_encoded, :description],
95 | :title => :title,
96 | :authors => [:author, :contributor, :dc_creator],
97 | :categories => :category,
98 | :last_updated => [:updated, :dc_date, :pubDate]
99 | }
100 |
101 | atomrss.entries.each do |atomrss_entry|
102 | feed_entry = Entry.new
103 | map_functions!(entry_mapping, atomrss_entry, feed_entry)
104 |
105 | # custom entry elements
106 | feed_entry.id = atomrss_entry.guid || atomrss_entry[:id] # entries are a Hash..
107 | feed_entry.copyright = atomrss_entry.copyright || (atomrss.respond_to?(:copyright) ? atomrss.copyright : nil)
108 |
109 | feed.entries << feed_entry
110 | end
111 |
112 | feed
113 | end
114 |
115 | def self.image(parser)
116 | if parser.respond_to?(:image) && parser.image
117 | if parser.image =~ // # RSS image contains an spec
118 | parser.image.scan(/(.*?)<\/url>/).to_s
119 | else
120 | parser.image # Atom contains just the url
121 | end
122 | elsif parser.respond_to?(:logo) && parser.logo
123 | parser.logo
124 | end
125 | end
126 |
127 | def self.feed_id(parser)
128 | overridden_value(parser, :id) || ("#{parser.link}" if parser.respond_to?(:link))
129 | end
130 |
131 | # gets the value returned from the method if it overriden, otherwise nil.
132 | def self.overridden_value(object, method)
133 | object.class.public_instance_methods(false).include? method
134 | end
135 |
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/lib/feed-normalizer/structures.rb:
--------------------------------------------------------------------------------
1 |
2 | module FeedNormalizer
3 |
4 | module Singular
5 |
6 | # If the method being called is a singular (in this simple case, does not
7 | # end with an 's'), then it calls the plural method, and calls the first
8 | # element. We're assuming that plural methods provide an array.
9 | #
10 | # Example:
11 | # Object contains an array called 'alphas', which looks like [:a, :b, :c].
12 | # Call object.alpha and :a is returned.
13 | def method_missing(name, *args)
14 | return self.send(:"#{name}s").first rescue super(name, *args)
15 | end
16 |
17 | def respond_to?(x, y=false)
18 | self.class::ELEMENTS.include?(x) || self.class::ELEMENTS.include?(:"#{x}s") || super(x, y)
19 | end
20 |
21 | end
22 |
23 | module ElementEquality
24 |
25 | def eql?(other)
26 | self == (other)
27 | end
28 |
29 | def ==(other)
30 | other.equal?(self) ||
31 | (other.instance_of?(self.class) &&
32 | self.class::ELEMENTS.all?{ |el| self.send(el) == other.send(el)} )
33 | end
34 |
35 | # Returns the difference between two Feed instances as a hash.
36 | # Any top-level differences in the Feed object as presented as:
37 | #
38 | # { :title => [content, other_content] }
39 | #
40 | # For differences at the items level, an array of hashes shows the diffs
41 | # on a per-entry basis. Only entries that differ will contain a hash:
42 | #
43 | # { :items => [
44 | # {:title => ["An article tile", "A new article title"]},
45 | # {:title => ["one title", "a different title"]} ]}
46 | #
47 | # If the number of items in each feed are different, then the count of each
48 | # is provided instead:
49 | #
50 | # { :items => [4,5] }
51 | #
52 | # This method can also be useful for human-readable feed comparison if
53 | # its output is dumped to YAML.
54 | def diff(other, elements = self.class::ELEMENTS)
55 | diffs = {}
56 |
57 | elements.each do |element|
58 | if other.respond_to?(element)
59 | self_value = self.send(element)
60 | other_value = other.send(element)
61 |
62 | next if self_value == other_value
63 |
64 | diffs[element] = if other_value.respond_to?(:diff)
65 | self_value.diff(other_value)
66 |
67 | elsif other_value.is_a?(Enumerable) && other_value.all?{|v| v.respond_to?(:diff)}
68 |
69 | if self_value.size != other_value.size
70 | [self_value.size, other_value.size]
71 | else
72 | enum_diffs = []
73 | self_value.each_with_index do |val, index|
74 | enum_diffs << val.diff(other_value[index], val.class::ELEMENTS)
75 | end
76 | enum_diffs.reject{|h| h.empty?}
77 | end
78 |
79 | else
80 | [other_value, self_value] unless other_value == self_value
81 | end
82 | end
83 | end
84 |
85 | diffs
86 | end
87 |
88 | end
89 |
90 | module ElementCleaner
91 | # Recursively cleans all elements in place.
92 | #
93 | # Only allow tags in whitelist. Always parse the html with a parser and delete
94 | # all tags that arent on the list.
95 | #
96 | # For feed elements that can contain HTML:
97 | # - feed.(title|description)
98 | # - feed.entries[n].(title|description|content)
99 | #
100 | def clean!
101 | self.class::SIMPLE_ELEMENTS.each do |element|
102 | val = self.send(element)
103 |
104 | send("#{element}=", (val.is_a?(Array) ?
105 | val.collect{|v| HtmlCleaner.flatten(v.to_s)} : HtmlCleaner.flatten(val.to_s)))
106 | end
107 |
108 | self.class::HTML_ELEMENTS.each do |element|
109 | send("#{element}=", HtmlCleaner.clean(self.send(element).to_s))
110 | end
111 |
112 | self.class::BLENDED_ELEMENTS.each do |element|
113 | self.send(element).collect{|v| v.clean!}
114 | end
115 | end
116 | end
117 |
118 | module TimeFix
119 | # Reparse any Time instances, due to RSS::Parser's redefinition of
120 | # certain aspects of the Time class that creates unexpected behaviour
121 | # when extending the Time class, as some common third party libraries do.
122 | # See http://code.google.com/p/feed-normalizer/issues/detail?id=13.
123 | def reparse(obj)
124 | @parsed ||= false
125 |
126 | return obj if @parsed
127 |
128 | if obj.is_a?(Time)
129 | @parsed = true
130 | Time.at(obj) rescue obj
131 | end
132 | end
133 | end
134 |
135 | module RewriteRelativeLinks
136 | def rewrite_relative_links(text, url)
137 | if host = url_host(url)
138 | text.to_s.gsub(/(href|src)=('|")\//, '\1=\2http://' + host + '/')
139 | else
140 | text
141 | end
142 | end
143 |
144 | private
145 | def url_host(url)
146 | URI.parse(url).host rescue nil
147 | end
148 | end
149 |
150 |
151 | # Represents a feed item entry.
152 | # Available fields are:
153 | # * content
154 | # * description
155 | # * title
156 | # * date_published
157 | # * urls / url
158 | # * id
159 | # * authors / author
160 | # * copyright
161 | # * categories
162 | class Entry
163 | include Singular, ElementEquality, ElementCleaner, TimeFix, RewriteRelativeLinks
164 |
165 | HTML_ELEMENTS = [:content, :description, :title]
166 | SIMPLE_ELEMENTS = [:date_published, :urls, :id, :authors, :copyright, :categories, :last_updated]
167 | BLENDED_ELEMENTS = []
168 |
169 | ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
170 |
171 | attr_accessor(*ELEMENTS)
172 |
173 | def initialize
174 | @urls = []
175 | @authors = []
176 | @categories = []
177 | @date_published, @content = nil
178 | end
179 |
180 | undef date_published
181 | def date_published
182 | @date_published = reparse(@date_published)
183 | end
184 |
185 | undef content
186 | def content
187 | @content = rewrite_relative_links(@content, url)
188 | end
189 |
190 | end
191 |
192 | # Represents the root element of a feed.
193 | # Available fields are:
194 | # * title
195 | # * description
196 | # * id
197 | # * last_updated
198 | # * copyright
199 | # * authors / author
200 | # * urls / url
201 | # * image
202 | # * generator
203 | # * items / channel
204 | class Feed
205 | include Singular, ElementEquality, ElementCleaner, TimeFix
206 |
207 | # Elements that can contain HTML fragments.
208 | HTML_ELEMENTS = [:title, :description]
209 |
210 | # Elements that contain 'plain' Strings, with HTML escaped.
211 | SIMPLE_ELEMENTS = [:id, :last_updated, :copyright, :authors, :urls, :image, :generator, :ttl, :skip_hours, :skip_days]
212 |
213 | # Elements that contain both HTML and escaped HTML.
214 | BLENDED_ELEMENTS = [:items]
215 |
216 | ELEMENTS = HTML_ELEMENTS + SIMPLE_ELEMENTS + BLENDED_ELEMENTS
217 |
218 | attr_accessor(*ELEMENTS)
219 | attr_accessor(:parser)
220 |
221 | alias :entries :items
222 |
223 | def initialize(wrapper)
224 | # set up associations (i.e. arrays where needed)
225 | @urls = []
226 | @authors = []
227 | @skip_hours = []
228 | @skip_days = []
229 | @items = []
230 | @parser = wrapper.parser.to_s
231 | @last_updated = nil
232 | end
233 |
234 | undef last_updated
235 | def last_updated
236 | @last_updated = reparse(@last_updated)
237 | end
238 |
239 | def channel() self end
240 |
241 | end
242 |
243 | end
244 |
245 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/CHANGELOG:
--------------------------------------------------------------------------------
1 | 0.2.2 (16-03-2012)
2 |
3 | * Simplified code.
4 |
5 | 0.2.1 (11-03-2012)
6 |
7 | * Better memory handling.
8 |
9 | * Little speed improvements.
10 |
11 | * Ruby 1.9 compatible?
12 |
13 | 0.2.0 (11-07-2009)
14 |
15 | * Return 0 instead of 0.0 in case of empty strings.
16 |
17 | * Added specific support for arrays.
18 |
19 | * Added specific support for arrays of strings.
20 |
21 | * Added generic support for all (?) kind of sequences.
22 |
23 | * Moved a lot of code to the C world.
24 |
25 | 0.1.1 (06-10-2008)
26 |
27 | * If one of the strings was both the begin and the end of the
28 | other string, it would be stripped from both ends. Example:
29 | Levenshtein.distance("abracadabra", "abra") resulted in 3
30 | instead of 7. It's fixed now.
31 |
32 | 0.1.0 (24-05-2008)
33 |
34 | * First release.
35 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/LICENSE:
--------------------------------------------------------------------------------
1 | # Copyright Erik Veenstra
2 | #
3 | # This program is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU General Public License,
5 | # version 2, as published by the Free Software Foundation.
6 | #
7 | # This program is distributed in the hope that it will be
8 | # useful, but WITHOUT ANY WARRANTY; without even the implied
9 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
10 | # PURPOSE. See the GNU General Public License for more details.
11 | #
12 | # You should have received a copy of the GNU General Public
13 | # License along with this program; if not, write to the Free
14 | # Software Foundation, Inc., 59 Temple Place, Suite 330,
15 | # Boston, MA 02111-1307 USA.
16 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/README:
--------------------------------------------------------------------------------
1 | The Levenshtein distance is a metric for measuring the amount
2 | of difference between two sequences (i.e., the so called edit
3 | distance). The Levenshtein distance between two sequences is
4 | given by the minimum number of operations needed to transform
5 | one sequence into the other, where an operation is an
6 | insertion, deletion, or substitution of a single element.
7 |
8 | The two sequences can be two strings, two arrays, or two other
9 | objects responding to :each. All sequences are by generic
10 | (fast) C code.
11 |
12 | All objects in the sequences should respond to :hash and :eql?.
13 |
14 | More information about the Levenshtein distance algorithm:
15 | http://en.wikipedia.org/wiki/Levenshtein_distance .
16 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/VERSION:
--------------------------------------------------------------------------------
1 | 0.2.2
2 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/ext/levenshtein/.RUBYARCHDIR.time:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ttscoff/Slogger/55e443d7706250faa8e568aa76aa8f643d7528b6/lib/levenshtein-0.2.2/ext/levenshtein/.RUBYARCHDIR.time
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/ext/levenshtein/extconf.rb:
--------------------------------------------------------------------------------
1 | require "mkmf"
2 |
3 | dir_config("levenshtein")
4 |
5 | have_library("levenshtein_array")
6 | have_library("levenshtein_array_of_strings")
7 | have_library("levenshtein_generic")
8 | have_library("levenshtein_string")
9 |
10 | create_makefile("levenshtein/levenshtein_fast")
11 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/ext/levenshtein/levenshtein.h:
--------------------------------------------------------------------------------
1 | #ifdef RARRAY_PTR
2 | #else
3 | #define RARRAY_PTR(o) (RARRAY(o)->ptr)
4 | #define RARRAY_LEN(o) (RARRAY(o)->len)
5 | #endif
6 |
7 | #ifdef RSTRING_PTR
8 | #else
9 | #define RSTRING_PTR(o) (RSTRING(o)->ptr)
10 | #define RSTRING_LEN(o) (RSTRING(o)->len)
11 | #endif
12 |
13 | VALUE mLevenshtein;
14 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/ext/levenshtein/levenshtein_fast.bundle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ttscoff/Slogger/55e443d7706250faa8e568aa76aa8f643d7528b6/lib/levenshtein-0.2.2/ext/levenshtein/levenshtein_fast.bundle
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/ext/levenshtein/levenshtein_fast.c:
--------------------------------------------------------------------------------
1 | #include "ruby.h"
2 | #include "levenshtein.h"
3 |
4 | VALUE levenshtein_distance_fast(VALUE self, VALUE rb_o1, VALUE rb_o2, VALUE rb_threshold) {
5 | VALUE *p1, *p2;
6 | long l1, l2;
7 | long col, row;
8 | int threshold;
9 | int *prev_row, *curr_row, *temp_row;
10 | int curr_row_min, result;
11 | int value1, value2;
12 |
13 | /* Be sure that all equivalent objects in rb_o1 and rb_o2 (a.eql?(b) == true) are taken from a pool (a.equal?(b) == true). */
14 | /* This is done in levenshtein.rb by means of Util.pool. */
15 |
16 | /* Get the sizes of both arrays. */
17 |
18 | l1 = RARRAY_LEN(rb_o1);
19 | l2 = RARRAY_LEN(rb_o2);
20 |
21 | /* Get the pointers of both arrays. */
22 |
23 | p1 = RARRAY_PTR(rb_o1);
24 | p2 = RARRAY_PTR(rb_o2);
25 |
26 | /* Convert Ruby's threshold to C's threshold. */
27 |
28 | if (!NIL_P(rb_threshold)) {
29 | threshold = FIX2INT(rb_threshold);
30 | } else {
31 | threshold = -1;
32 | }
33 |
34 | /* The Levenshtein algorithm itself. */
35 |
36 | /* s1= */
37 | /* ERIK */
38 | /* */
39 | /* 01234 */
40 | /* s2=V 11234 */
41 | /* E 21234 */
42 | /* E 32234 */
43 | /* N 43334 <- prev_row */
44 | /* S 54444 <- curr_row */
45 | /* T 65555 */
46 | /* R 76566 */
47 | /* A 87667 */
48 |
49 | /* Allocate memory for both rows */
50 |
51 | prev_row = (int*) ALLOC_N(int, (l1+1));
52 | curr_row = (int*) ALLOC_N(int, (l1+1));
53 |
54 | /* Initialize the current row. */
55 |
56 | for (col=0; col<=l1; col++) {
57 | curr_row[col] = col;
58 | }
59 |
60 | for (row=1; row<=l2; row++) {
61 | /* Copy the current row to the previous row. */
62 |
63 | temp_row = prev_row;
64 | prev_row = curr_row;
65 | curr_row = temp_row;
66 |
67 | /* Calculate the values of the current row. */
68 |
69 | curr_row[0] = row;
70 | curr_row_min = row;
71 |
72 | for (col=1; col<=l1; col++) {
73 | /* Equal (cost=0) or substitution (cost=1). */
74 |
75 | value1 = prev_row[col-1] + ((p1[col-1] == p2[row-1]) ? 0 : 1);
76 |
77 | /* Insertion if it's cheaper than substitution. */
78 |
79 | value2 = prev_row[col]+1;
80 | if (value2 < value1) {
81 | value1 = value2;
82 | }
83 |
84 | /* Deletion if it's cheaper than substitution. */
85 |
86 | value2 = curr_row[col-1]+1;
87 | if (value2 < value1) {
88 | value1 = value2;
89 | }
90 |
91 | /* Keep track of the minimum value on this row. */
92 |
93 | if (value1 < curr_row_min) {
94 | curr_row_min = value1;
95 | }
96 |
97 | curr_row[col] = value1;
98 | }
99 |
100 | /* Return nil as soon as we exceed the threshold. */
101 |
102 | if (threshold > -1 && curr_row_min >= threshold) {
103 | free(prev_row);
104 | free(curr_row);
105 |
106 | return Qnil;
107 | }
108 | }
109 |
110 | /* The result is the last value on the last row. */
111 |
112 | result = curr_row[l1];
113 |
114 | free(prev_row);
115 | free(curr_row);
116 |
117 | /* Return the Ruby version of the result. */
118 |
119 | return INT2FIX(result);
120 | }
121 |
122 | void Init_levenshtein_fast() {
123 | mLevenshtein = rb_const_get(rb_mKernel, rb_intern("Levenshtein"));
124 |
125 | rb_define_singleton_method(mLevenshtein, "distance_fast" , levenshtein_distance_fast, 3);
126 | }
127 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/lib/levenshtein.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | require File.join(File.dirname(__FILE__),"levenshtein/version.rb")
4 |
5 | module Levenshtein
6 | # Returns the Levenshtein distance as a number between 0.0 and
7 | # 1.0. It's basically the Levenshtein distance divided by the
8 | # size of the longest sequence.
9 |
10 | def self.normalized_distance(a1, a2, threshold=nil, options={})
11 | size = [a1.size, a2.size].max
12 |
13 | if a1.size == 0 and a2.size == 0
14 | 0.0
15 | elsif a1.size == 0
16 | a2.size.to_f/size
17 | elsif a2.size == 0
18 | a1.size.to_f/size
19 | else
20 | if threshold
21 | if d = self.distance(a1, a2, (threshold*size).to_i+1)
22 | d.to_f/size
23 | else
24 | nil
25 | end
26 | else
27 | self.distance(a1, a2).to_f/size
28 | end
29 | end
30 | end
31 |
32 | # Returns the Levenshtein distance between two sequences.
33 | #
34 | # The two sequences can be two strings, two arrays, or two other
35 | # objects responding to :each. All sequences are by generic
36 | # (fast) C code.
37 | #
38 | # All objects in the sequences should respond to :hash and :eql?.
39 |
40 | def self.distance(a1, a2, threshold=nil, options={})
41 | a1, a2 = a1.scan(/./), a2.scan(/./) if String === a1 and String === a2
42 | a1, a2 = Util.pool(a1, a2)
43 |
44 | # Handle some basic circumstances.
45 |
46 | return 0 if a1 == a2
47 | return a2.size if a1.empty?
48 | return a1.size if a2.empty?
49 |
50 | if threshold
51 | return nil if (a1.size-a2.size) >= threshold
52 | return nil if (a2.size-a1.size) >= threshold
53 | return nil if (a1-a2).size >= threshold
54 | return nil if (a2-a1).size >= threshold
55 | end
56 |
57 | # Remove the common prefix and the common postfix.
58 |
59 | l1 = a1.size
60 | l2 = a2.size
61 |
62 | offset = 0
63 | no_more_optimizations = true
64 |
65 | while offset < l1 and offset < l2 and a1[offset].equal?(a2[offset])
66 | offset += 1
67 |
68 | no_more_optimizations = false
69 | end
70 |
71 | while offset < l1 and offset < l2 and a1[l1-1].equal?(a2[l2-1])
72 | l1 -= 1
73 | l2 -= 1
74 |
75 | no_more_optimizations = false
76 | end
77 |
78 | if no_more_optimizations
79 | distance_fast_or_slow(a1, a2, threshold, options)
80 | else
81 | l1 -= offset
82 | l2 -= offset
83 |
84 | a1 = a1[offset, l1]
85 | a2 = a2[offset, l2]
86 |
87 | distance(a1, a2, threshold, options)
88 | end
89 | end
90 |
91 | def self.distance_fast_or_slow(a1, a2, threshold, options) # :nodoc:
92 | if respond_to?(:distance_fast) and options[:force_slow]
93 | distance_fast(a1, a2, threshold) # Implemented in C.
94 | else
95 | distance_slow(a1, a2, threshold) # Implemented in Ruby.
96 | end
97 | end
98 |
99 | def self.distance_slow(a1, a2, threshold) # :nodoc:
100 | crow = (0..a1.size).to_a
101 |
102 | 1.upto(a2.size) do |y|
103 | prow = crow
104 | crow = [y]
105 |
106 | 1.upto(a1.size) do |x|
107 | crow[x] = [prow[x]+1, crow[x-1]+1, prow[x-1]+(a1[x-1].equal?(a2[y-1]) ? 0 : 1)].min
108 | end
109 |
110 | # Stop analysing this sequence as soon as the best possible
111 | # result for this sequence is bigger than the best result so far.
112 | # (The minimum value in the next row will be equal to or greater
113 | # than the minimum value in this row.)
114 |
115 | return nil if threshold and crow.min >= threshold
116 | end
117 |
118 | crow[-1]
119 | end
120 |
121 | module Util # :nodoc:
122 | def self.pool(*args)
123 | # So we can compare pointers instead of objects (equal?() instead of ==()).
124 |
125 | pool = {}
126 |
127 | args.collect do |arg|
128 | a = []
129 |
130 | arg.each do |o|
131 | a << pool[o] ||= o
132 | end
133 |
134 | a
135 | end
136 | end
137 | end
138 | end
139 |
140 | # begin
141 | # require File.join(File.dirname(__FILE__),"levenshtein/levenshtein_fast") # Compiled by RubyGems.
142 | # rescue LoadError
143 | # begin
144 | # require "levenshtein_fast" # Compiled by the build script.
145 | # rescue LoadError
146 | # $stderr.puts "WARNING: Couldn't find the fast C implementation of Levenshtein. Using the much slower Ruby version instead."
147 | # end
148 | # end
149 |
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/lib/levenshtein/levenshtein_fast.bundle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ttscoff/Slogger/55e443d7706250faa8e568aa76aa8f643d7528b6/lib/levenshtein-0.2.2/lib/levenshtein/levenshtein_fast.bundle
--------------------------------------------------------------------------------
/lib/levenshtein-0.2.2/lib/levenshtein/version.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | module Levenshtein
4 | VERSION = "0.2.2"
5 | end
6 |
--------------------------------------------------------------------------------
/lib/multimarkdown:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ttscoff/Slogger/55e443d7706250faa8e568aa76aa8f643d7528b6/lib/multimarkdown
--------------------------------------------------------------------------------
/lib/redirect.rb:
--------------------------------------------------------------------------------
1 | class RedirectFollower
2 | class TooManyRedirects < StandardError; end
3 |
4 | attr_accessor :url, :body, :redirect_limit, :response
5 |
6 | def initialize(url, limit=5)
7 | @url, @redirect_limit = url, limit
8 | end
9 |
10 | def resolve
11 | raise TooManyRedirects if redirect_limit < 0
12 |
13 | self.response = Net::HTTP.get_response(URI.parse(url))
14 | if response.kind_of?(Net::HTTPRedirection)
15 | self.url = redirect_url
16 | self.redirect_limit -= 1
17 | resolve
18 | end
19 |
20 | self.body = response.body
21 | self
22 | end
23 |
24 | def redirect_url
25 | if response['location'].nil?
26 | response.body.match(/]+)\">/i)[1]
27 | else
28 | response['location']
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/sociallogger.rb:
--------------------------------------------------------------------------------
1 | class SocialLogger
2 | def initialize(options = {})
3 | @debug = options['debug'] || false
4 | @config = options['config'] || {}
5 | end
6 | attr_accessor :debug, :config
7 | end
8 |
--------------------------------------------------------------------------------
/plugin_template.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: My New Logger
3 | Description: Brief description (one line)
4 | Author: [My Name](My URL)
5 | Configuration:
6 | option_1_name: [ "example_value1" , "example_value2", ... ]
7 | option_2_name: example_value
8 | Notes:
9 | - multi-line notes with additional description and information (optional)
10 | =end
11 |
12 | config = { # description and a primary key (username, url, etc.) required
13 | 'description' => ['Main description',
14 | 'additional notes. These will appear in the config file and should contain descriptions of configuration options',
15 | 'line 2, continue array as needed'],
16 | 'service_username' => '', # update the name and make this a string or an array if you want to handle multiple accounts.
17 | 'additional_config_option' => false
18 | 'tags' => '#social #blogging' # A good idea to provide this with an appropriate default setting
19 | }
20 | # Update the class key to match the unique classname below
21 | $slog.register_plugin({ 'class' => 'ServiceLogger', 'config' => config })
22 |
23 | # unique class name: leave '< Slogger' but change ServiceLogger (e.g. LastFMLogger)
24 | class ServiceLogger < Slogger
25 | # every plugin must contain a do_log function which creates a new entry using the DayOne class (example below)
26 | # @config is available with all of the keys defined in "config" above
27 | # @timespan and @dayonepath are also available
28 | # returns: nothing
29 | def do_log
30 | if @config.key?(self.class.name)
31 | config = @config[self.class.name]
32 | # check for a required key to determine whether setup has been completed or not
33 | if !config.key?('service_username') || config['service_username'] == []
34 | @log.warn(" has not been configured or an option is invalid, please edit your slogger_config file.")
35 | return
36 | else
37 | # set any local variables as needed
38 | username = config['service_username']
39 | end
40 | else
41 | @log.warn(" has not been configured or a feed is invalid, please edit your slogger_config file.")
42 | return
43 | end
44 | @log.info("Logging posts for #{username}")
45 |
46 | additional_config_option = config['additional_config_option'] || false
47 | tags = config['tags'] || ''
48 | tags = "\n\n#{@tags}\n" unless @tags == ''
49 |
50 | today = @timespan
51 |
52 | # Perform necessary functions to retrieve posts
53 |
54 | # create an options array to pass to 'to_dayone'
55 | # all options have default fallbacks, so you only need to create the options you want to specify
56 | options = {}
57 | options['content'] = "## Post title\n\nContent#{tags}"
58 | options['datestamp'] = Time.now.utc.iso8601
59 | options['starred'] = true
60 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
61 |
62 | # Create a journal entry
63 | # to_dayone accepts all of the above options as a hash
64 | # generates an entry base on the datestamp key or defaults to "now"
65 | sl = DayOne.new
66 | sl.to_dayone(options)
67 |
68 | # To create an image entry, use `sl.to_dayone(options) if sl.save_image(imageurl,options['uuid'])`
69 | # save_image takes an image path and a uuid that must be identical the one passed to to_dayone
70 | # save_image returns false if there's an error
71 |
72 | end
73 |
74 | def helper_function(args)
75 | # add helper functions within the class to handle repetitive tasks
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/plugins/appnetlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: App.net Logger
3 | Version: 1.1
4 | Description: Logs today's posts to App.net.
5 | Notes:
6 | appnet_usernames is an array of App.net user names
7 | Author: [Alan Schussman](http://schussman.com)
8 | Configuration:
9 | appnet_usernames: [ ]
10 | appnet_tags: "#social #appnet"
11 | appnet_save_replies: false
12 | appnet_digest: true
13 | Notes:
14 |
15 | =end
16 | config = {
17 | 'appnet_description' => [
18 | 'Logs posts for today from App.net',
19 | 'appnet_usernames is an array of App.net user names'],
20 | 'appnet_usernames' => [ ],
21 | 'appnet_tags' => '#social #appnet',
22 | 'appnet_save_replies' => false,
23 | 'appnet_digest' => true
24 | }
25 | $slog.register_plugin({ 'class' => 'AppNetLogger', 'config' => config })
26 |
27 | require 'rexml/document'
28 | require 'rss/dublincore'
29 |
30 | class AppNetLogger < Slogger
31 | def linkify(input)
32 | input.gsub(/@(\S+)/,"[\\0](https://alpha.app.net/\\1)").gsub(/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@^=%&\/~\+#])?/,"<\\0>")
33 | end
34 |
35 | def do_log
36 | if config.key?(self.class.name)
37 | config = @config[self.class.name]
38 | if !config.key?('appnet_usernames') || config['appnet_usernames'] == [] || config['appnet_usernames'].empty?
39 | @log.warn("App.net user names have not been configured, please edit your slogger_config file.")
40 | return
41 | end
42 | else
43 | @log.warn("App.net user names have not been configured, please edit your slogger_config file.")
44 | return
45 | end
46 |
47 | sl = DayOne.new
48 | config['appnet_tags'] ||= ''
49 | tags = "\n\n(#{config['appnet_tags']})\n" unless config['appnet_tags'] == ''
50 | today = @timespan.to_i
51 |
52 | @log.info("Getting App.net posts for #{config['appnet_usernames'].length} feeds")
53 | if config['save_appnet_replies']
54 | @log.info("replies: true")
55 | end
56 | output = ''
57 |
58 | config['appnet_usernames'].each do |user|
59 | begin
60 | rss_feed = "https://alpha-api.app.net/feed/rss/users/@"+ user + "/posts"
61 |
62 | url = URI.parse rss_feed
63 |
64 | http = Net::HTTP.new url.host, url.port
65 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
66 | http.use_ssl = true
67 |
68 | rss_content = nil
69 |
70 | http.start do |agent|
71 | rss_content = agent.get(url.path).read_body
72 | end
73 |
74 | rss = RSS::Parser.parse(rss_content, true)
75 | feed_output = ''
76 | rss.items.each { |item|
77 | item_date = Time.parse(item.date.to_s) + Time.now.gmt_offset
78 | if item_date > @timespan
79 | content = ''
80 | item.title = item.title.gsub(/^@#{user}: /,'').strip # remove user's own name from front of post
81 | item.title = item.title.gsub(/\n/,"\n ") if config['appnet_digest'] # fix for multi-line posts displayed in markdown
82 | if item.title =~ /^@/
83 | if config['appnet_save_replies']
84 | if config['appnet_digest']
85 | feed_output += "* [#{item_date.strftime(@time_format)}](#{item.link}) #{linkify(item.title)}#{content}\n"
86 | else
87 | feed_output = "#{linkify(item.title)}\n"
88 | end
89 | end
90 | else
91 | if config['appnet_digest']
92 | feed_output += "* [#{item_date.strftime(@time_format)}](#{item.link}) #{linkify(item.title)}#{content}\n"
93 | else
94 | feed_output = "#{linkify(item.title)}\n"
95 | end
96 | end
97 | unless config['appnet_digest']
98 | output = feed_output
99 | unless output == ''
100 | options = {}
101 | options['datestamp'] = Time.parse(item.date.to_s).utc.iso8601
102 | options['content'] = "## App.net [post](#{item.link}) by [@#{user}](#{rss.channel.link})\n#{output}#{tags}"
103 | sl.to_dayone(options)
104 | end
105 | end
106 | else
107 | break
108 | end
109 | }
110 | if config['appnet_digest']
111 | output += "#### [#{rss.channel.title}](#{rss.channel.link})\n\n" + feed_output + "\n" unless feed_output == ''
112 | end
113 | rescue Exception => e
114 | puts "Error getting posts for #{rss_feed}"
115 | p e
116 | return ''
117 | end
118 | end
119 | unless output == '' || !config['appnet_digest']
120 | options = {}
121 | options['content'] = "## App.net posts\n\n#{output}#{tags}"
122 | sl.to_dayone(options)
123 | end
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/plugins/flickrlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Flickr Logger
3 | Version: 1.0
4 | Description: Logs today's photos from Flickr.
5 | Notes:
6 | Get your Flickr ID at
7 | Get your Flickr API key at
8 | Author: [Brett Terpstra](http://brettterpstra.com)
9 | Configuration:
10 | flickr_api_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX'
11 | flickr_ids: [flickr_id1[, flickr_id2...]]
12 | flickr_tags: "#social #photo"
13 | Notes:
14 |
15 | =end
16 | config = {
17 | 'flickr_description' => [
18 | 'Logs today\'s photos from Flickr.',
19 | 'flickr_ids is an array of one or more IDs',
20 | 'flickr_datetype can be the "upload" or "taken" date to be used',
21 | 'Get your Flickr ID at ',
22 | 'Get your Flickr API key at '],
23 | 'flickr_api_key' => '',
24 | 'flickr_ids' => [],
25 | 'flickr_datetype' => 'upload',
26 | 'flickr_tags' => '#social #photo'
27 | }
28 | $slog.register_plugin({ 'class' => 'FlickrLogger', 'config' => config })
29 |
30 | require 'rexml/document'
31 |
32 | class FlickrLogger < Slogger
33 |
34 | # download images to local files and create day one entries
35 | # images is an array of hashes: { 'content' => 'photo title', 'date' => 'iso8601 date', 'url' => 'source url' }
36 | def download_images(images)
37 |
38 | images.each do |image|
39 | options = {}
40 | options['content'] = image['content']
41 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
42 | options['datestamp'] = image['date']
43 | sl = DayOne.new
44 | path = sl.save_image(image['url'],options['uuid'])
45 | sl.store_single_photo(path,options) unless path == false
46 | end
47 |
48 | return true
49 | end
50 |
51 | def do_log
52 | if @config.key?(self.class.name)
53 | config = @config[self.class.name]
54 | if !config.key?('flickr_ids') || config['flickr_ids'] == []
55 | @log.warn("Flickr users have not been configured, please edit your slogger_config file.")
56 | return
57 | end
58 | else
59 | @log.warn("Flickr users have not been configured, please edit your slogger_config file.")
60 | return
61 | end
62 |
63 | sl = DayOne.new
64 | config['flickr_tags'] ||= ''
65 | tags = config['flickr_tags'] == '' ? '' : "\n\n(#{config['flickr_tags']})\n"
66 | today = @timespan.to_i
67 |
68 | @log.info("Getting Flickr images for #{config['flickr_ids'].join(', ')}")
69 | images = []
70 | begin
71 | config['flickr_ids'].each do |user|
72 | open("https://www.flickr.com/services/rest/?method=flickr.people.getPublicPhotos&api_key=#{config['flickr_api_key']}&user_id=#{user}&extras=description,date_upload,date_taken,url_m&per_page=15") { |f|
73 | REXML::Document.new(f.read).elements.each("rsp/photos/photo") { |photo|
74 | if config.key?('flickr_datetype') && config['flickr_datetype'] == 'taken'
75 | # import images in dayone using the date/time when the photo was taken
76 | photo_date = photo.attributes["datetaken"].to_s
77 | photo_date = DateTime.now
78 | # compensate for current timezone (will not compensate for DST, because it takes the current system timezone)
79 | zone = photo_date.zone
80 | photo_date = DateTime.parse(photo.attributes["datetaken"] + zone)
81 | photo_date = photo_date.strftime('%s').to_s
82 | break unless Time.at(photo_date.to_i).utc > @timespan.utc
83 | image_date = Time.at(photo_date.to_i).utc.iso8601
84 | else
85 | # import images in dayone using the date/time when the photo was taken
86 | photo_date = photo.attributes["dateupload"].to_s
87 | break unless Time.at(photo_date.to_i) > @timespan
88 | image_date = Time.at(photo_date.to_i).utc.iso8601
89 | end
90 | url = photo.attributes["url_m"]
91 | content = "## " + photo.attributes['title']
92 | content += "\n\n" + photo.attributes['content'] unless photo.attributes['content'].nil?
93 | content += tags
94 | images << { 'content' => content, 'date' => image_date, 'url' => url }
95 | }
96 | }
97 | end
98 |
99 | rescue Exception => e
100 | puts "Error getting photos for #{config['flickr_ids'].join(', ')}"
101 | p e
102 | return ''
103 | end
104 |
105 | if images.length == 0
106 | @log.info("No new Flickr images found")
107 | return ''
108 | else
109 | @log.info("Found #{images.length} images")
110 | end
111 |
112 | begin
113 | self.download_images(images)
114 | rescue Exception => e
115 | raise "Failure downloading images"
116 | p e
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/plugins/foursquarelogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Foursquare Logger
3 | Version: 1.0
4 | Description: Checks Foursquare feed once a day for that day's posts.
5 | Author: [Jeff Mueller](https://github.com/jeffmueller)
6 | Configuration:
7 | foursquare_feed: "https://feeds.foursquare.com/history/yourfoursquarehistory.rss"
8 | foursquare_tags: "#social #checkins"
9 | Notes:
10 | Find your feed at (in RSS option)
11 | =end
12 |
13 | default_config = {
14 | 'description' => [
15 | 'foursquare_feed must refer to the address of your personal feed.','Your feed should be available at '],
16 | 'foursquare_feed' => "",
17 | 'foursquare_tags' => "#social #checkins"
18 | }
19 | $slog.register_plugin({ 'class' => 'FoursquareLogger', 'config' => default_config })
20 |
21 | class FoursquareLogger < Slogger
22 | def do_log
23 | if @config.key?(self.class.name)
24 | config = @config[self.class.name]
25 | if !config.key?('foursquare_feed') || config['foursquare_feed'] == ''
26 | @log.warn("Foursquare feed has not been configured, please edit your slogger_config file.")
27 | return
28 | else
29 | @feed = config['foursquare_feed']
30 | end
31 | else
32 | @log.warn("Foursquare feed has not been configured, please edit your slogger_config file.")
33 | return
34 | end
35 |
36 | @log.info("Getting Foursquare checkins")
37 |
38 | config['foursquare_tags'] ||= ''
39 | @tags = "\n\n(#{config['foursquare_tags']})\n" unless config['foursquare_tags'] == ''
40 | @debug = config['debug'] || false
41 |
42 | entrytext = ''
43 | rss_content = ''
44 | begin
45 | url = URI.parse(@feed)
46 |
47 | http = Net::HTTP.new url.host, url.port
48 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
49 | http.use_ssl = true
50 |
51 | res = nil
52 |
53 | http.start do |agent|
54 | rss_content = agent.get(url.path).read_body
55 | end
56 |
57 | rescue Exception => e
58 | @log.error("ERROR fetching Foursquare feed")
59 | # p e
60 | end
61 | content = ''
62 | rss = RSS::Parser.parse(rss_content, false)
63 | rss.items.each { |item|
64 | break if Time.parse(item.pubDate.to_s) < @timespan
65 | content += "* [#{item.title}](#{item.link})\n"
66 | }
67 | if content != ''
68 | entrytext = "## Foursquare Checkins for #{@timespan.strftime(@date_format)}\n\n" + content + "\n#{@tags}"
69 | end
70 | DayOne.new.to_dayone({'content' => entrytext}) unless entrytext == ''
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/plugins/githublogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Github Logger
3 | Version: 1.1
4 | Description: Logs daily Github activity for the specified user
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | github_user: githubuser
8 | github_tags: "#social #coding"
9 | Notes:
10 |
11 | =end
12 | # NOTE: Requires json gem
13 | config = {
14 | 'description' => ['Logs daily Github activity for the specified user','github_user should be your Github username'],
15 | 'github_user' => '',
16 | 'github_tags' => '#social #coding',
17 | }
18 | $slog.register_plugin({ 'class' => 'GithubLogger', 'config' => config })
19 |
20 | class GithubLogger < Slogger
21 |
22 | def do_log
23 | if @config.key?(self.class.name)
24 | config = @config[self.class.name]
25 | if !config.key?('github_user') || config['github_user'] == ''
26 | @log.warn("Github user has not been configured or is invalid, please edit your slogger_config file.")
27 | return
28 | end
29 | else
30 | @log.warn("Github user has not been configured, please edit your slogger_config file.")
31 | return
32 | end
33 | @log.info("Logging Github activity for #{config['github_user']}")
34 | begin
35 | url = URI.parse "https://api.github.com/users/#{config['github_user'].strip}/events"
36 |
37 | http = Net::HTTP.new url.host, url.port
38 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
39 | http.use_ssl = true
40 |
41 | res = nil
42 |
43 | http.start do |agent|
44 | res = agent.get(url.path).read_body
45 | end
46 | rescue Exception => e
47 | @log.error("ERROR retrieving Github url: #{url}")
48 | # p e
49 | end
50 |
51 | return false if res.nil?
52 | json = JSON.parse(res)
53 |
54 | output = ""
55 |
56 | json.each {|action|
57 | date = Time.parse(action['created_at'])
58 | if date > @timespan
59 | case action['type']
60 | when "PushEvent"
61 | if !action["repo"]
62 | action['repo'] = {"name" => "unknown repo"}
63 | end
64 | output += "* Pushed to branch *#{action['payload']['ref'].gsub(/refs\/heads\//,'')}* of [#{action['repo']['name']}](#{action['url']})\n"
65 | action['payload']['commits'].each do |commits|
66 | output += " * #{commits["message"]}\n"
67 | end
68 | when "GistEvent"
69 | output += "* Created gist [#{action['payload']['name']}](#{action['payload']['url']})\n"
70 | output += " * #{action['payload']['desc'].gsub(/\n/," ")}\n" unless action['payload']['desc'].nil?
71 | when "WatchEvent"
72 | if action['payload']['action'] == "started"
73 | output += "* Started watching [#{action['repo']['owner']}/#{action['repo']['name']}](#{action['repo']['url']})\n"
74 | output += " * #{action['repo']['description'].gsub(/\n/," ")}\n" unless action['repo']['description'].nil?
75 | end
76 | end
77 | else
78 | break
79 | end
80 | }
81 |
82 | return false if output.strip == ""
83 | entry = "## Github activity for #{Time.now.strftime(@date_format)}:\n\n#{output}\n(#{config['github_tags']})"
84 | DayOne.new.to_dayone({ 'content' => entry })
85 | end
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/plugins/goodreadslogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Goodreads Logger
3 | Version: 1.0
4 | Description: Creates separate entries for books you finished today
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | goodreads_feed: "feedurl"
8 | goodreads_star_posts: true
9 | goodreads_save_image: true
10 | goodreads_tags: "#social #reading"
11 | Notes:
12 | - goodreads_save_image will save the book cover as the main image for the entry
13 | - goodreads_feed is a string containing the RSS feed for your read books
14 | - goodreads_star_posts will create a starred post for new books
15 | - goodreads_tags are tags you want to add to every entry, e.g. "#social #reading"
16 | =end
17 | require 'rexml/document';
18 | config = {
19 | 'description' => ['goodreads_save_image will save the book cover as the main image for the entry',
20 | 'goodreads_feed is a string containing the RSS feed for your read books',
21 | 'goodreads_star_posts will create a starred post for new books',
22 | 'goodreads_tags are tags you want to add to every entry, e.g. "#social #reading"'],
23 | 'goodreads_feed' => '',
24 | 'goodreads_save_image' => false,
25 | 'goodreads_star_posts' => false,
26 | 'goodreads_tags' => '#social #reading'
27 | }
28 | $slog.register_plugin({ 'class' => 'GoodreadsLogger', 'config' => config })
29 |
30 | class GoodreadsLogger < Slogger
31 | # Debugger.start
32 | def do_log
33 | feed = ''
34 | if @config.key?(self.class.name)
35 | @grconfig = @config[self.class.name]
36 | if !@grconfig.key?('goodreads_feed') || @grconfig['goodreads_feed'] == ''
37 | @log.warn("Goodreads feed has not been configured or is invalid, please edit your slogger_config file.")
38 | return
39 | else
40 | feed = @grconfig['goodreads_feed']
41 | end
42 | else
43 | @log.warn("Goodreads feed has not been configured or is invalid, please edit your slogger_config file.")
44 | return
45 | end
46 | @log.info("Logging read books from Goodreads")
47 |
48 | retries = 0
49 | success = false
50 | until success
51 | if parse_feed(feed)
52 | success = true
53 | else
54 | break if $options[:max_retries] == retries
55 | retries += 1
56 | @log.error("Error parsing Goodreads feed, retrying (#{retries}/#{$options[:max_retries]})")
57 | sleep 2
58 | end
59 | unless success
60 | @log.fatal("Could not parse feed #{feed}")
61 | end
62 | end
63 | end
64 |
65 | def parse_feed(rss_feed)
66 | markdownify = @grconfig['goodreads_markdownify_posts']
67 | unless (markdownify.is_a? TrueClass or markdownify.is_a? FalseClass)
68 | markdownify = false
69 | end
70 | starred = @grconfig['goodreads_star_posts']
71 | unless (starred.is_a? TrueClass or starred.is_a? FalseClass)
72 | starred = false
73 | end
74 | save_image = @grconfig['goodreads_save_image']
75 | unless (save_image.is_a? TrueClass or save_image.is_a? FalseClass)
76 | save_image = false
77 | end
78 |
79 | tags = @grconfig['goodreads_tags'] || ''
80 | tags = "\n\n(#{tags})\n" unless tags == ''
81 |
82 | begin
83 | #rss_content = ""
84 |
85 | feed_download_response = Net::HTTP.get_response(URI.parse(rss_feed));
86 | xml_data = feed_download_response.body;
87 |
88 | doc = REXML::Document.new(xml_data);
89 | doc.root.each_element('//item') { |item|
90 | content = ''
91 | item_date = Time.parse(item.elements['pubDate'].text)
92 | if item_date > @timespan
93 | imageurl = false
94 | # read items are those where the guid type begins with 'Review'
95 | #debugger
96 | #next if !item.elements['guid'].text.start_with?('Review')
97 | #desc = item.elements['book_description'].cdatas().join
98 | if save_image
99 | imageurl = item.elements['book_large_image_url'].cdatas().join rescue false
100 | end
101 | content += "* Author: #{item.elements['author_name'].text}\n"
102 | content += "* My rating: #{item.elements['user_rating'].text} / 5\n" rescue ''
103 | if item.elements['title'].to_s =~ /CDATA/
104 | title = item.elements['title'].cdatas().join
105 | else
106 | title = item.elements['title'].text
107 | end
108 | review = item.elements['user_review'].cdatas().join rescue ''
109 | if !review.empty?
110 | content += "* My review:\n\n #{review}\n" rescue ''
111 | end
112 | content = content != '' ? "\n\n#{content}" : ''
113 |
114 | options = {}
115 | options['content'] = "Finished reading [#{title}](#{item.elements['link'].cdatas().join})#{content}#{tags}"
116 | options['datestamp'] = Time.parse(item.elements['pubDate'].text).utc.iso8601
117 | options['starred'] = starred
118 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
119 | sl = DayOne.new
120 | if imageurl
121 | sl.to_dayone(options) if sl.save_image(imageurl,options['uuid'])
122 | else
123 | sl.to_dayone(options)
124 | end
125 |
126 | else
127 | break
128 | end
129 | }
130 | rescue Exception => e
131 | p e
132 | return false
133 | end
134 | return true
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/plugins/instapaperlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Instapaper Logger
3 | Version: 1.0.1
4 | Description: Logs today's additions to Instapaper.
5 | Notes:
6 | instapaper_feeds is an array of Instapaper RSS feeds
7 | - Find the RSS feed for any folder by inspecting the HTML source for a URL with type "application/rss+xml",
8 | and then prefix with 'https://www.instapaper.com/'
9 | - Seems to now need to use a secure connction to Instapaper
10 | Author: [Brett Terpstra](http://brettterpstra.com)
11 | Configuration:
12 | instapaper_feeds: [ 'https://www.instapaper.com/rss/106249/XXXXXXXXXXXXXX']
13 | instapaper_tags: "#social #reading"
14 | =end
15 | config = {
16 | 'instapaper_description' => [
17 | 'Logs today\'s posts to Instapaper.',
18 | 'instapaper_feeds is an array of one or more RSS feeds',
19 | 'Find the RSS feed for any folder at the bottom of a web interface page'],
20 | 'instapaper_feeds' => [],
21 | 'instapaper_include_content_preview' => true,
22 | 'instapaper_tags' => '#social #reading'
23 | }
24 | $slog.register_plugin({ 'class' => 'InstapaperLogger', 'config' => config })
25 |
26 | require 'rexml/document'
27 |
28 | class InstapaperLogger < Slogger
29 | def do_log
30 | if @config.key?(self.class.name)
31 | config = @config[self.class.name]
32 | if !config.key?('instapaper_feeds') || config['instapaper_feeds'] == [] || config['instapaper_feeds'].empty?
33 | @log.warn("Instapaper feeds have not been configured, please edit your slogger_config file.")
34 | return
35 | end
36 | else
37 | @log.warn("Instapaper feeds have not been configured, please edit your slogger_config file.")
38 | return
39 | end
40 |
41 | sl = DayOne.new
42 | config['instapaper_tags'] ||= ''
43 | tags = "\n\n(#{config['instapaper_tags']})\n" unless config['instapaper_tags'] == ''
44 | today = @timespan.to_i
45 |
46 | @log.info("Getting Instapaper posts for #{config['instapaper_feeds'].length} accounts")
47 | output = ''
48 |
49 | config['instapaper_feeds'].each do |rss_feed|
50 | begin
51 | rss_content = ""
52 | open(rss_feed) do |f|
53 | rss_content = f.read
54 | end
55 |
56 | rss = RSS::Parser.parse(rss_content, false)
57 | feed_output = ''
58 | rss.items.each { |item|
59 | item_date = Time.parse(item.pubDate.to_s)
60 | if item_date > @timespan
61 | content = item.description.gsub(/\n/,"\n ") unless item.description == ''
62 | feed_output += "* [#{item.title}](#{item.link})\n"
63 | feed_output += "\n #{content}\n" if config['instapaper_include_content_preview'] == true
64 | else
65 | # The archive orders posts inconsistenly so older items can
66 | # show up before newer ones
67 | if rss.channel.title != "Instapaper: Archive"
68 | break
69 | end
70 | end
71 | }
72 | output += "#### #{rss.channel.title}\n\n" + feed_output + "\n" unless feed_output == ''
73 | rescue Exception => e
74 | raise "Error getting posts for #{rss_feed}"
75 | p e
76 | return ''
77 | end
78 | end
79 | unless output.strip == ''
80 | options = {}
81 | options['content'] = "## Instapaper reading\n\n#{output}#{tags}"
82 | sl.to_dayone(options)
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/plugins/lastfmlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Last.fm Logger
3 | Version: 1.3
4 | Description: Logs playlists and loved tracks for the day
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | lastfm_user: lastfmusername
8 | lastfm_tags: "#social #blogging"
9 | Notes:
10 | - added timestamps option
11 | =end
12 | config = {
13 | 'lastfm_description' => [
14 | 'Logs songs scrobbled for time period.',
15 | 'lastfm_user is your Last.fm username.',
16 | 'lastfm_feeds is an array that determines whether it grabs recent tracks, loved tracks, or both',
17 | 'lastfm_include_timestamps (true/false) will add a timestamp prefix based on @time_format to each song'
18 | ],
19 | 'lastfm_include_timestamps' => false,
20 | 'lastfm_user' => '',
21 | 'lastfm_feeds' => ['recent', 'loved'],
22 | 'lastfm_tags' => '#social #music'
23 | }
24 | $slog.register_plugin({ 'class' => 'LastFMLogger', 'config' => config })
25 |
26 | class LastFMLogger < Slogger
27 | def get_fm_feed(feed)
28 | begin
29 | rss_content = false
30 | feed_url = URI.parse(feed)
31 | feed_url.open do |f|
32 | rss_content = f.read
33 | end
34 | return rss_content
35 | rescue
36 | return false
37 | end
38 | end
39 |
40 | def do_log
41 | if @config.key?(self.class.name)
42 | config = @config[self.class.name]
43 | if !config.key?('lastfm_user') || config['lastfm_user'] == ''
44 | @log.warn("Last.fm has not been configured, please edit your slogger_config file.")
45 | return
46 | else
47 | feeds = config['feeds']
48 | end
49 | else
50 | @log.warn("Last.fm has not been configured, please edit your slogger_config file.")
51 | return
52 | end
53 |
54 | config['lastfm_tags'] ||= ''
55 | tags = "\n\n(#{config['lastfm_tags']})\n" unless config['lastfm_tags'] == ''
56 |
57 | config['lastfm_feeds'] ||= ['recent', 'loved']
58 |
59 | feeds = []
60 | feeds << {'title'=>"Listening To", 'feed' => "http://ws.audioscrobbler.com/2.0/user/#{config['lastfm_user']}/recenttracks.rss?limit=100"} if config['lastfm_feeds'].include?('recent')
61 | feeds << {'title'=>"Loved Tracks", 'feed' => "http://ws.audioscrobbler.com/2.0/user/#{config['lastfm_user']}/lovedtracks.rss?limit=100"} if config['lastfm_feeds'].include?('loved')
62 |
63 | today = @timespan
64 |
65 | @log.info("Getting Last.fm playlists for #{config['lastfm_user']}")
66 |
67 | feeds.each do |rss_feed|
68 | entrytext = ''
69 | rss_content = try { get_fm_feed(rss_feed['feed'])}
70 | unless rss_content
71 | @log.error("Failed to retrieve #{rss_feed['title']} for #{config['lastfm_user']}")
72 | break
73 | end
74 | content = ''
75 | rss = RSS::Parser.parse(rss_content, false)
76 |
77 | # define a hash to store song count and a hash to link song title to the last.fm URL
78 | songs_count = {}
79 | title_to_link = {}
80 |
81 | rss.items.each { |item|
82 | timestamp = Time.parse(item.pubDate.to_s)
83 | break if timestamp < today
84 | ts = config['lastfm_include_timestamps'] ? "#{timestamp.strftime(@time_format)} | " : ""
85 | title = ts + String(item.title).e_link()
86 | link = String(item.link).e_link()
87 |
88 | # keep track of URL for each song title
89 | title_to_link[title] = link
90 |
91 | # store play counts in hash
92 | if songs_count[title].nil?
93 | songs_count[title] = 1
94 | else
95 | songs_count[title] += 1
96 | end
97 | }
98 |
99 | # loop over each song and make final output as appropriate
100 | # (depending on whether there was 1 play or more)
101 | songs_count.each { |k, v|
102 |
103 | # a fudge because I couldn't seem to access this hash value directly in
104 | # the if statement
105 | link = title_to_link[k]
106 |
107 | if v == 1
108 | content += "* [#{k}](#{link})\n"
109 | else
110 | content += "* [#{k}](#{link}) (#{v} plays)\n"
111 | end
112 | }
113 |
114 | if content != ''
115 | entrytext = "## #{rss_feed['title']} for #{today.strftime(@date_format)}\n\n" + content + "\n#{tags}"
116 | end
117 | DayOne.new.to_dayone({'content' => entrytext}) unless entrytext == ''
118 | end
119 | end
120 |
121 | def try(&action)
122 | retries = 0
123 | success = false
124 | until success || $options[:max_retries] == retries
125 | result = yield
126 | if result
127 | success = true
128 | else
129 | retries += 1
130 | @log.error("Error performing action, retrying (#{retries}/#{$options[:max_retries]})")
131 | sleep 2
132 | end
133 | end
134 | result
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/plugins/pinboardlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Pinboard Logger
3 | Version: 1.0
4 | Description: Logs today's bookmarks from Pinboard.in.
5 | Notes:
6 | pinboard_feeds is an array of Pinboard RSS feeds
7 | - There's an RSS button on every user/tag page on Pinboard, copy the link
8 | Author: [Brett Terpstra](http://brettterpstra.com)
9 | Configuration:
10 | pinboard_feeds: [ 'http://feeds.pinboard.in/rss/u:username/']
11 | pinboard_tags: "#social #bookmarks"
12 | pinboard_digest: true
13 | Notes:
14 |
15 | =end
16 | config = {
17 | 'pinboard_description' => [
18 | 'Logs bookmarks for today from Pinboard.in.',
19 | 'pinboard_feeds is an array of one or more Pinboard RSS feeds',
20 | 'pinboard_digest true will group all new bookmarks into one post, false will split them into individual posts dated when the bookmark was created'],
21 | 'pinboard_feeds' => [],
22 | 'pinboard_tags' => '#social #bookmarks',
23 | 'pinboard_save_hashtags' => true,
24 | 'pinboard_digest' => true
25 | }
26 | $slog.register_plugin({'class' => 'PinboardLogger', 'config' => config})
27 |
28 | require 'rexml/document'
29 | require 'rss/dublincore'
30 |
31 | class PinboardLogger < Slogger
32 | def split_days(bookmarks)
33 | # tweets.push({:text => tweet_text, :date => tweet_date, :screen_name => screen_name, :images => tweet_images, :id => tweet_id})
34 | dated_bookmarks = {}
35 | bookmarks.each {|mark|
36 | date = mark[:date].strftime('%Y-%m-%d')
37 | dated_bookmarks[date] = [] unless dated_bookmarks[date]
38 | dated_bookmarks[date].push(mark)
39 | }
40 | dated_bookmarks
41 | end
42 |
43 | def digest_entry(bookmarks, tags)
44 | bookmarks.reverse.map do |t|
45 | t[:content]
46 | end.join("\n") << "\n#{tags.strip}"
47 | end
48 |
49 | def do_log
50 | if @config.key?(self.class.name)
51 | config = @config[self.class.name]
52 | if !config.key?('pinboard_feeds') || config['pinboard_feeds'] == [] || config['pinboard_feeds'].empty?
53 | @log.warn("Pinboard feeds have not been configured, please edit your slogger_config file.")
54 | return
55 | end
56 | else
57 | @log.warn("Pinboard feeds have not been configured, please edit your slogger_config file.")
58 | return
59 | end
60 |
61 | sl = DayOne.new
62 | config['pinboard_tags'] ||= ''
63 | tags = "\n\n(#{config['pinboard_tags'].strip})\n" unless config['pinboard_tags'] == ''
64 | today = @timespan.to_i
65 |
66 | @log.info("Getting Pinboard bookmarks for #{config['pinboard_feeds'].length} feeds")
67 | feed_link = ''
68 | feed_output = []
69 |
70 | config['pinboard_feeds'].each do |rss_feed|
71 | begin
72 | rss_content = ""
73 | open(rss_feed) do |f|
74 | rss_content = f.read
75 | end
76 |
77 | rss = RSS::Parser.parse(rss_content, false)
78 |
79 | rss.items.each { |item|
80 | feed_output = [] unless config['pinboard_digest']
81 | item_date = Time.parse(item.date.to_s) + Time.now.gmt_offset
82 | if item_date > @timespan
83 | content = ''
84 | post_tags = ''
85 | if config['pinboard_digest']
86 | content = "\n\t" + item.description.gsub(/\n/, "\n\t").strip unless item.description.nil?
87 | else
88 | content = "\n> " + item.description.gsub(/\n/, "\n> ").strip unless item.description.nil?
89 | end
90 | content = "#{content}\n" unless content == ''
91 | if config['pinboard_save_hashtags']
92 | post_tags = "\n\t\t" + item.dc_subject.split(' ').map { |tag| "##{tag}" }.join(' ') + "\n" unless item.dc_subject.nil?
93 | end
94 |
95 | feed_output.push({:date => Time.parse(item.date.to_s), :content => "#{config['pinboard_digest'] ? '* ' : ''}[#{item.title.gsub(/\n/, ' ').strip}](#{item.link})\n#{content}#{post_tags}"})
96 | else
97 | break
98 | end
99 | output = feed_output[0][:content] unless feed_output[0].nil? or config['pinboard_digest']
100 | unless output == '' || config['pinboard_digest']
101 | options = {}
102 | options['datestamp'] = feed_output[0][:date].utc.iso8601
103 | options['content'] = "## New Pinboard bookmark\n#{output}#{tags.strip}"
104 | sl.to_dayone(options)
105 | end
106 | }
107 | feed_link = "[#{rss.channel.title}](#{rss.channel.link})" unless feed_output.empty?
108 | rescue Exception => e
109 | puts "Error getting posts for #{rss_feed}"
110 | p e
111 | return
112 | end
113 | end
114 | unless feed_link == '' || !config['pinboard_digest']
115 | dated_marks = split_days(feed_output)
116 | dated_marks.each {|k,v|
117 | content = "## Pinboard bookmarks\n\n### #{feed_link} on #{Time.parse(k).strftime(@date_format)}\n\n"
118 | content << digest_entry(v, tags)
119 | sl.to_dayone({'content' => content, 'datestamp' => Time.parse(k).utc.iso8601})
120 | }
121 | end
122 | return
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/plugins/pocketlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Pocket Logger
3 | Version: 2.0
4 | Description: Logs today's additions to Pocket.
5 | Notes:
6 | pocket_username is a string with your Pocket username
7 | Author: [Brett Terpstra](http://brettterpstra.com)
8 | Configuration:
9 | pocket_username: 'your_username'
10 | pocket_passwd: "your_password" // if RSS Feed password protection is on
11 | pocket_tags: "#social #reading"
12 | Notes:
13 |
14 | =end
15 | config = {
16 | 'pocket_description' => [
17 | 'Logs today\'s posts to Pocket.',
18 | 'pocket_username is a string with your Pocket username',
19 | 'pocket_passwd is a string with your Pocket password'],
20 | 'pocket_username' => '',
21 | 'pocket_passwd' => '',
22 | 'pocket_tags' => '#social #reading'
23 | }
24 | $slog.register_plugin({ 'class' => 'PocketLogger', 'config' => config })
25 |
26 | require 'rexml/document'
27 |
28 | class PocketLogger < Slogger
29 | def do_log
30 | if @config.key?(self.class.name)
31 | config = @config[self.class.name]
32 | if !config.key?('pocket_username') || config['pocket_username'].nil?
33 | @log.warn("Pocket username has not been configured, please edit your slogger_config file.")
34 | return
35 | end
36 | else
37 | @log.warn("Pocket has not been configured, please edit your slogger_config file.")
38 | return
39 | end
40 |
41 | sl = DayOne.new
42 | config['pocket_tags'] ||= ''
43 | username = config['pocket_username']
44 | password = config['pocket_passwd']
45 | tags = "\n\n(#{config['pocket_tags']})\n" unless config['pocket_tags'] == ''
46 | today = @timespan
47 |
48 | @log.info("Getting Pocket posts for #{username}")
49 | output = ''
50 |
51 | ["read","unread"].each {|kind|
52 | rss_feed = "https://getpocket.com/users/#{username.strip}/feed/#{kind}"
53 | title = case kind
54 | when "read" then "### Items read today:"
55 | when "unread" then "### Items saved today:"
56 | end
57 |
58 | begin
59 | rss_content = ""
60 | open(rss_feed, http_basic_authentication: [username, password]) do |f|
61 | rss_content = f.read
62 | end
63 | tempoutput = ""
64 | rss = RSS::Parser.parse(rss_content, false)
65 |
66 | rss.items.each { |item|
67 | item_date = Time.parse(item.pubDate.to_s)
68 | if item_date > @timespan
69 | tempoutput += "* [#{item.title}](#{item.link})\n"
70 | else
71 | break
72 | end
73 | }
74 | output += "#{title}\n\n#{tempoutput}\n\n" unless tempoutput == ""
75 |
76 | rescue Exception => e
77 | puts "Error getting posts for #{username}"
78 | p e
79 | return ''
80 | end
81 | }
82 | unless output == ''
83 | options = {}
84 | options['content'] = "## Pocket reading\n\n#{output}#{tags}"
85 | sl.to_dayone(options)
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/plugins/rsslogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: RSS Logger
3 | Version: 1.0
4 | Description: Logs any RSS feed as a digest and checks for new posts for the current day
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | feeds: [ "feed url 1" , "feed url 2", ... ]
8 | tags: "#social #rss"
9 | Notes:
10 | - rss_feeds is an array of feeds separated by commas, a single feed is fine, but it should be inside of brackets `[]`
11 | - rss_tags are tags you want to add to every entry, e.g. "#social #rss"
12 | =end
13 |
14 | config = {
15 | 'description' => ['Logs any RSS feed as a digest and checks for new posts for the current day',
16 | 'feeds is an array of feeds separated by commas, a single feed is fine, but it should be inside of brackets `[]`',
17 | 'tags are tags you want to add to every entry, e.g. "#social #rss"'],
18 | 'feeds' => [],
19 | 'tags' => '#social #rss'
20 | }
21 | $slog.register_plugin({ 'class' => 'RSSLogger', 'config' => config })
22 |
23 | class RSSLogger < Slogger
24 | def do_log
25 | feeds = []
26 | if @config.key?(self.class.name)
27 | @rssconfig = @config[self.class.name]
28 | if !@rssconfig.key?('feeds') || @rssconfig['feeds'] == [] || @rssconfig['feeds'].nil?
29 | @log.warn("RSS feeds have not been configured or a feed is invalid, please edit your slogger_config file.")
30 | return
31 | else
32 | feeds = @rssconfig['feeds']
33 | end
34 | else
35 | @log.warn("RSS2 feeds have not been configured or a feed is invalid, please edit your slogger_config file.")
36 | return
37 | end
38 | @log.info("Logging rss posts for feeds #{feeds.join(', ')}")
39 |
40 | feeds.each do |rss_feed|
41 | retries = 0
42 | success = false
43 | until success
44 | if parse_feed(rss_feed)
45 | success = true
46 | else
47 | break if $options[:max_retries] == retries
48 | retries += 1
49 | @log.error("Error parsing #{rss_feed}, retrying (#{retries}/#{$options[:max_retries]})")
50 | sleep 2
51 | end
52 | end
53 |
54 | unless success
55 | @log.fatal("Could not parse feed #{rss_feed}")
56 | end
57 | end
58 | end
59 |
60 | def parse_feed(rss_feed)
61 |
62 | tags = @rssconfig['tags'] || ''
63 | tags = "\n\n(#{tags})\n" unless tags == ''
64 |
65 | today = @timespan
66 | begin
67 |
68 | rss_content = ""
69 |
70 | if rss_feed =~ /^https/
71 | url = URI.parse(rss_feed)
72 |
73 | http = Net::HTTP.new url.host, url.port
74 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
75 | http.use_ssl = true
76 |
77 | res = nil
78 |
79 | http.start do |agent|
80 | rss_content = agent.get(url.path).read_body
81 | end
82 | else
83 | open(rss_feed) do |f|
84 | rss_content = f.read
85 | end
86 | end
87 |
88 | rss = RSS::Parser.parse(rss_content, false)
89 | feed_items = []
90 | rss.items.each { |item|
91 | rss_date = item.date || item.updated
92 | item_date = Time.parse(rss_date.to_s) + Time.now.gmt_offset
93 | if item_date > today
94 | feed_items.push("* [#{item.title.gsub(/\n+/,' ').strip}](#{item.link})")
95 | else
96 | break
97 | end
98 | }
99 |
100 | if feed_items.length > 0
101 | options = {}
102 | options['content'] = "## #{rss.channel.title.gsub(/\n+/,' ').strip}\n\n#{feed_items.reverse.join("\n")}#{tags}"
103 | sl = DayOne.new
104 | sl.to_dayone(options)
105 | end
106 | rescue Exception => e
107 | p e
108 | return false
109 | end
110 | return true
111 | end
112 |
113 | def permalink(uri,redirect_count=0)
114 | max_redirects = 10
115 | options = {}
116 | url = URI.parse(uri)
117 | http = Net::HTTP.new(url.host, url.port)
118 | begin
119 | request = Net::HTTP::Get.new(url.request_uri)
120 | response = http.request(request)
121 | response['location'].gsub(/\?utm.*/,'')
122 | rescue
123 | puts "Error expanding #{uri}"
124 | uri
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/plugins_disabled/asanalogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Asana Logger
3 | Description: Logs daily Asana activity
4 | Author: [Tom Torsney-Weir](http://www.tomtorsneyweir.com)
5 | Configuration:
6 | asana_api_key: you can get this from your profile on asana
7 | Notes:
8 | - asana_api_key is a string with your personal Asana api key
9 | =end
10 |
11 | config = { # description and a primary key (username, url, etc.) required
12 | 'description' => ['Logs daily Asana activity',
13 | 'asana_api_key is a string with your personal Asana API key.',
14 | 'This can be obtained from your profile screen in Asana.'],
15 | 'asana_api_key' => '',
16 | 'asana_star_posts' => true,
17 | 'asana_tags' => '#tasks'
18 | }
19 | # Update the class key to match the unique classname below
20 | $slog.register_plugin({ 'class' => 'AsanaLogger', 'config' => config })
21 |
22 | require "json"
23 | require "net/https"
24 |
25 | class AsanaLogger < Slogger
26 | # @config is available with all of the keys defined in "config" above
27 | # @timespan and @dayonepath are also available
28 | def do_log
29 | if @config.key?(self.class.name)
30 | config = @config[self.class.name]
31 | # check for a required key to determine whether setup has been completed or not
32 | if !config.key?('asana_api_key') || config['asana_api_key'] == []
33 | @log.warn("AsanaLogger has not been configured or an option is invalid, please edit your slogger_config file.")
34 | return
35 | else
36 | # set any local variables as needed
37 | api_key = config['asana_api_key']
38 | end
39 | else
40 | @log.warn("AsanaLogger has not been configured or a feed is invalid, please edit your slogger_config file.")
41 | return
42 | end
43 | @log.info("Logging AsanaLogger posts")
44 |
45 | asana_tags = config['asana_tags'] || ''
46 | asana_tags = "\n\n#{asana_tags}\n" unless asana_tags == ''
47 |
48 | # Perform necessary functions to retrieve posts
49 | content = ""
50 | get_workspaces(api_key).each do |ws_info|
51 | ws_id = ws_info['id']
52 | ws_name = ws_info['name']
53 | @log.info("Getting tasks for #{ws_name}")
54 | tasks = asana(api_key, "/workspaces/#{ws_id}/tasks?include_archived=true&assignee=me")['data']
55 | finished_tasks = tasks.map {|t| asana(api_key, "/tasks/#{t['id']}")['data']}
56 | finished_tasks.select! {|t| t['completed'] and Time.parse(t['completed_at']) > @timespan}
57 | unless finished_tasks.empty?
58 | content += "### Tasks finished today:\n\n"
59 | finished_tasks.each do |t|
60 | content += "* #{format_task(t)}\n"
61 | end
62 | content += "\n"
63 | end
64 | added_tasks = tasks.map {|t| asana(api_key, "/tasks/#{t['id']}")['data']}
65 | added_tasks.select! {|t| Time.parse(t['created_at']) > @timespan}
66 | unless added_tasks.empty?
67 | content += "### Tasks added today:\n\n"
68 | added_tasks.each do |t|
69 | content += "* #{format_task(t)}\n"
70 | end
71 | content += "\n"
72 | end
73 | end
74 |
75 | # set up day one post
76 | options = {}
77 | options['datestamp'] = Time.now.utc.iso8601
78 | options['starred'] = config['asana_star_posts']
79 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
80 |
81 | # Create a journal entry
82 | unless content.empty?
83 | sl = DayOne.new
84 | options['content'] = "## Asana activity\n\n#{content}#{asana_tags}"
85 | sl.to_dayone(options)
86 | end
87 | end
88 |
89 | def get_workspaces(key)
90 | user_info = asana(key, '/users/me')
91 | user_info['data']['workspaces']
92 | end
93 |
94 | def format_task(task)
95 | projs = task['projects'] || []
96 | projs.map! {|p| p['name']}
97 | if projs.empty?
98 | proj_names = ""
99 | else
100 | proj_names = " (#{projs.join(', ')})"
101 | end
102 | ws_id = task['workspace']['id']
103 | task_url = "https://app.asana.com/0/#{ws_id}/#{task['id']}"
104 | "[#{task['name']}#{proj_names}](#{task_url})"
105 | end
106 |
107 | def asana(api_key, req_url, params={})
108 | # set up HTTPS connection
109 | uri = URI.parse("https://app.asana.com/api/1.0#{req_url}")
110 | http = Net::HTTP.new(uri.host, uri.port)
111 | http.use_ssl = true
112 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER
113 |
114 | # set up the request
115 | req = Net::HTTP::Get.new(uri.request_uri, params)
116 | req.basic_auth(api_key, '')
117 |
118 | # issue the request
119 | res = http.start { |http| http.request(req) }
120 |
121 | # output
122 | body = JSON.parse(res.body)
123 | if body['errors'] then
124 | raise "Server returned an error: #{body['errors'][0]['message']}"
125 | end
126 | body
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/plugins_disabled/facebookifttt.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Facebook / IFTTT logger
3 | Description: Parses Facebook posts logged by IFTTT.com
4 | Author: [hargrove](https://github.com/spiritofnine)
5 | Configuration:
6 | facebook_ifttt_input_file: "/path/to/dropbox/ifttt/facebook.txt"
7 | Notes:
8 | - Configure IFTTT to log Facebook status posts to a text file.
9 | - You can use the recipe at https://ifttt.com/recipes/56242
10 | - and personalize if for your Dropbox set up.
11 | -
12 | - Unless you change it, the recipe will write to the following
13 | - location:
14 | -
15 | - {Dropbox path}/AppData/ifttt/facebook/facebook.md.txt
16 | -
17 | - You probably don't want that, so change it in the recipe accordingly.
18 | -
19 | - On a standard Dropbox install on OS X, the Dropbox path is
20 | -
21 | - /Users/username/Dropbox
22 | -
23 | - so the full path is:
24 | -
25 | - /Users/username/Dropbox/AppData/ifttt/facebook/facebook.md.txt
26 | -
27 | - You should set facebook_ifttt_input_file to this value, substituting username appropriately.
28 | =end
29 |
30 | require 'date'
31 |
32 | config = {
33 | 'description' => ['Parses Facebook posts logged by IFTTT.com',
34 | 'facebook_ifttt_input_file is a string pointing to the location of the file created by IFTTT.',
35 | 'The recipe at https://ifttt.com/recipes/56242 determines that location.'],
36 | 'facebook_ifttt_input_file' => '',
37 | 'facebook_ifttt_star' => false,
38 | 'facebook_ifttt_tags' => '#social #blogging'
39 | }
40 |
41 | $slog.register_plugin({ 'class' => 'FacebookIFTTTLogger', 'config' => config })
42 |
43 | class FacebookIFTTTLogger < Slogger
44 | require 'date'
45 | require 'time'
46 |
47 | def do_log
48 | if @config.key?(self.class.name)
49 | config = @config[self.class.name]
50 | if !config.key?('facebook_ifttt_input_file') || config['facebook_ifttt_input_file'] == []
51 | @log.warn("FacebookIFTTTLogger has not been configured or an option is invalid, please edit your slogger_config file.")
52 | return
53 | end
54 | else
55 | @log.warn("FacebookIFTTTLogger has not been configured or a feed is invalid, please edit your slogger_config file.")
56 | return
57 | end
58 |
59 | tags = config['facebook_ifttt_tags'] || ''
60 | tags = "\n\n#{@tags}\n" unless @tags == ''
61 |
62 | inputFile = config['facebook_ifttt_input_file']
63 |
64 | @log.info("Logging FacebookIFTTTLogger posts at #{inputFile}")
65 |
66 | regPost = /^Post: /
67 | regDate = /^Date: /
68 | ampm = /(AM|PM)\Z/
69 | pm = /PM\Z/
70 |
71 | last_run = @timespan
72 |
73 | ready = false
74 | inpost = false
75 | posttext = ""
76 |
77 | options = {}
78 | options['starred'] = config['facebook_ifttt_star']
79 |
80 | f = File.new(File.expand_path(inputFile))
81 | content = f.read
82 | f.close
83 |
84 | if !content.empty?
85 | each_selector = RUBY_VERSION < "1.9.2" ? :each : :each_line
86 | content.send(each_selector) do | line|
87 | if line =~ regDate
88 | inpost = false
89 | line = line.strip
90 | line = line.gsub(regDate, "")
91 | line = line.gsub(" at ", ' ')
92 | line = line.gsub(',', '')
93 |
94 | month, day, year, time = line.split
95 | parseTime = DateTime.parse(time).strftime("%H:%M")
96 | hour,min = parseTime.split(/:/)
97 |
98 | month = Date::MONTHNAMES.index(month)
99 | ltime = Time.local(year, month, day, hour, min, 0, 0)
100 | date = ltime.to_i
101 |
102 | if not date > last_run.to_i
103 | posttext = ""
104 | next
105 | end
106 |
107 | options['datestamp'] = ltime.utc.iso8601
108 | ready = true
109 | elsif line =~ regPost or inpost == true
110 | inpost = true
111 | line = line.gsub(regPost, "")
112 | posttext += line
113 | ready = false
114 | end
115 |
116 | if ready
117 | sl = DayOne.new
118 | options['content'] = "#### FacebookIFTTT\n\n#{posttext}\n\n#{tags}"
119 | sl.to_dayone(options)
120 | ready = false
121 | posttext = ""
122 | end
123 | end
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/plugins_disabled/feedlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: FeedLogger
3 | Description: Logs any RSS or Atom feed and checks for new posts for the current day
4 | Author: [masukomi](http://masukomi.org)
5 | Configuration:
6 | feeds: [ "feed url 1" , "feed url 2", ... ]
7 | markdownify_posts: true
8 | star_posts: true
9 | tags: "#social #blogging"
10 | Notes:
11 | - if found, the first image in the post will be saved as the main image for the entry
12 | - atom_feeds is an array of feeds separated by commas, a single feed is fine, but it should be inside of brackets `[]`
13 | - markdownify_posts will convert links and emphasis in the post to Markdown for display in Day One
14 | - star_posts will create a starred post for new atom posts
15 | - atom_tags are tags you want to add to every entry, e.g. "#social #blogging"
16 | =end
17 |
18 | require 'feed-normalizer'
19 |
20 | config = {
21 | 'description' => ['Logs any feed and checks for new posts for the current day',
22 | 'feeds is an array of feeds separated by commas, a single feed is fine, but it should be inside of brackets `[]`',
23 | 'markdownify_posts will convert links and emphasis in the post to Markdown for display in Day One',
24 | 'star_posts will create a starred post for new posts',
25 | 'tags are tags you want to add to every entry, e.g. "#social #blogging"'],
26 | 'feeds' => [],
27 | 'markdownify_posts' => true,
28 | 'star_posts' => false,
29 | 'tags' => '#social #blogging'
30 | }
31 | $slog.register_plugin({ 'class' => 'FeedLogger', 'config' => config })
32 |
33 | class FeedLogger < Slogger
34 | def do_log
35 | feeds = []
36 | if @config.key?(self.class.name)
37 | config = @config[self.class.name]
38 | if !config.key?('feeds') || config['feeds'] == []
39 | @log.warn("Feeds have not been configured or a feed is invalid, please edit your slogger_config file.")
40 | return
41 | else
42 | feeds = config['feeds']
43 | end
44 | else
45 | @log.warn("Feeds have not been configured or a feed is invalid, please edit your slogger_config file.")
46 | return
47 | end
48 | @log.info("Logging posts for feeds #{feeds.join(', ')}")
49 |
50 | feeds.each do |feed_url|
51 | retries = 0
52 | success = false
53 | until success
54 | if parse_feed(config, feed_url)
55 | success = true
56 | else
57 | break if $options[:max_retries] == retries
58 | retries += 1
59 | @log.error("Error parsing #{feed_url}, retrying (#{retries}/#{$options[:max_retries]})")
60 | sleep 2
61 | end
62 | end
63 |
64 | unless success
65 | @log.fatal("Could not parse feed #{feed_url}")
66 | end
67 | end
68 | end
69 |
70 | def parse_feed(config, feed_url)
71 | markdownify = config['markdownify_posts']
72 | unless (markdownify.is_a? TrueClass or markdownify.is_a? FalseClass)
73 | markdownify = true
74 | end
75 | starred = config['star_posts']
76 | unless (starred.is_a? TrueClass or starred.is_a? FalseClass)
77 | starred = true
78 | end
79 | tags = config['tags'] || ''
80 | tags = "\n\n#{@tags}\n" unless @tags == ''
81 |
82 | today = @timespan
83 | begin
84 |
85 | feed = FeedNormalizer::FeedNormalizer.parse open(feed_url)
86 | feed.entries.each { |entry|
87 | entry_date = nil
88 | if (entry.date_published and entry.date_published.to_s.length() > 0)
89 | entry_date = Time.parse(entry.date_published.to_s)
90 | elsif (entry.last_updated and entry.last_updated.to_s.length() > 0)
91 | @log.info("Entry #{entry.title} - no published date found\n\t\tUsing last update date instead.")
92 | entry_date = Time.parse(entry.last_updated.to_s)
93 | else
94 | @log.info("Entry #{entry.title} - no published date found\n\t\tUsing current Time instead.")
95 | entry_date = Time.now()
96 | end
97 | if entry_date > today
98 | @log.info("parsing #{entry.title} w/ date: #{entry.date_published}")
99 | imageurl = false
100 | image_match = entry.content.match(/src="(http:.*?\.(jpg|png)(\?.*?)?)"/i) rescue nil
101 | imageurl = image_match[1] unless image_match.nil?
102 | content = ''
103 | begin
104 | if markdownify
105 | content = entry.content.markdownify rescue ''
106 | else
107 | content = entry.content rescue ''
108 | end
109 | rescue => e
110 | @log.error("problem parsing content: #{e}")
111 | end
112 |
113 | options = {}
114 | options['content'] = "## [#{entry.title.gsub(/\n+/,' ').strip}](#{entry.url})\n\n#{content.strip}#{tags}"
115 | options['datestamp'] = entry.date_published.utc.iso8601 rescue Time.now.utc.iso8601
116 | options['starred'] = starred
117 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
118 |
119 | sl = DayOne.new
120 | if imageurl
121 | sl.to_dayone(options) if sl.save_image(imageurl,options['uuid'])
122 | else
123 | sl.to_dayone(options)
124 | end
125 | else
126 | break
127 | end
128 | }
129 | rescue Exception => e
130 | p e
131 | return false
132 | end
133 | return true
134 | end
135 |
136 | def permalink(uri,redirect_count=0)
137 | max_redirects = 10
138 | options = {}
139 | url = URI.parse(uri)
140 | http = Net::HTTP.new(url.host, url.port)
141 | begin
142 | request = Net::HTTP::Get.new(url.request_uri)
143 | response = http.request(request)
144 | response['location'].gsub(/\?utm.*/,'')
145 | rescue
146 | puts "Error expanding #{uri}"
147 | uri
148 | end
149 | end
150 | end
151 |
--------------------------------------------------------------------------------
/plugins_disabled/flickrlogger_rss.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Flickr Logger
3 | Description: Logs today's photos from Flickr RSS feed. Get your Flickr ID at
4 | Author: [Brett Terpstra](http://brettterpstra.com)
5 | Configuration:
6 | flickr_ids: [flickr_id1[, flickr_id2...]]
7 | flickr_tags: "#social #photo"
8 | Notes:
9 | - This version uses the RSS feed. This can take up to four hours to update, which is why I wrote the default API version. I'm impatient
10 | =end
11 | config = {
12 | 'description' => ['flickr_ids should be an array with one or more Flickr user ids (http://idgettr.com/)']
13 | 'flickr_ids' => [],
14 | 'flickr_tags' => '#social #photo'
15 | }
16 | $slog.register_plugin({ 'class' => 'FlickrLogger', 'config' => config })
17 |
18 | require 'rexml/document'
19 |
20 | class FlickrLogger < Slogger
21 |
22 | # download images to local files and create day one entries
23 | # images is an array of hashes: { 'content' => 'photo title', 'date' => 'iso8601 date', 'url' => 'source url' }
24 | def download_images(images)
25 |
26 | images.each do |image|
27 | options = {}
28 | options['content'] = image['content']
29 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
30 | sl = DayOne.new
31 | path = sl.save_image(image['url'],options['uuid'])
32 | sl.store_single_photo(path,options)
33 | end
34 |
35 | return true
36 | end
37 |
38 | def do_log
39 | if config.key?(self.class.name)
40 | config = @config[self.class.name]
41 | if !config.key?('flickr_ids') || config['flickr_ids'] == []
42 | @log.warn("Flickr users have not been configured, please edit your slogger_config file.")
43 | return
44 | end
45 | else
46 | @log.warn("Flickr users have not been configured, please edit your slogger_config file.")
47 | return
48 | end
49 |
50 | sl = DayOne.new
51 | config['flickr_tags'] ||= ''
52 | tags = "\n\n#{config['flickr_tags']}\n" unless config['flickr_tags'] == ''
53 |
54 | @log.info("Getting Flickr images for #{config['flickr_ids'].join(', ')}")
55 | url = URI.parse("http://api.flickr.com/services/feeds/photos_public.gne?ids=#{config['flickr_ids'].join(',')}")
56 |
57 | begin
58 | begin
59 | res = Net::HTTP.get_response(url).body
60 | rescue Exception => e
61 | raise "Failure getting response from Flickr"
62 | p e
63 | end
64 | images = []
65 | REXML::Document.new(res).elements.each("feed/entry") { |photo|
66 | today = @timespan
67 | photo_date = Time.parse(photo.elements['published'].text)
68 | break if photo_date < today
69 | content = "## " + photo.elements['title'].text
70 | url = photo.elements['link'].text
71 | content += "\n\n" + photo.elements['content'].text.markdownify unless photo.elements['content'].text == ''
72 | images << { 'content' => content, 'date' => photo_date.utc.iso8601, 'url' => url }
73 | }
74 | rescue Exception => e
75 | puts "Error getting photos for #{config['flickr_ids'].join(', ')}"
76 | p e
77 | return ''
78 | end
79 | if images.length == 0
80 | @log.info("No new Flickr images found")
81 | return ''
82 | else
83 | @log.info("Found #{images.length} images")
84 | end
85 |
86 | begin
87 | self.download_images(images)
88 | rescue Exception => e
89 | raise "Failure downloading images"
90 | p e
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/plugins_disabled/gaugeslogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Gaug.es Logger
3 | Version: 1.1
4 | Description: Logs daily traffic status from http://get.gaug.es/
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | gauges_token: XXXXXXXXXXXXXXXX
8 | gauges_tags: "#social #sitestats"
9 | Notes:
10 | This plugin requires an API token to run. Run slogger -o "gauges" to create the
11 | configuration section for it. Then log into your Guag.es account and go to:
12 | and create a new client.
13 | Copy the key for that client and set 'gauges_token:' to it in your slogger_config.
14 | =end
15 | # NOTE: Requires json gem
16 | config = {
17 | 'description' => ['Logs daily traffic status from http://get.gaug.es/','Create a key for gauges_token at https://secure.gaug.es/dashboard#/account/clients'],
18 | 'gauges_token' => '',
19 | 'gauges_tags' => '#social #sitestats',
20 | }
21 | $slog.register_plugin({ 'class' => 'GaugesLogger', 'config' => config })
22 |
23 | class GaugesLogger < Slogger
24 |
25 | def gauges_api_call(key,type)
26 | type.gsub!(/https:\/\/secure.gaug.es\//,'') if type =~ /^https:/
27 |
28 | res = nil
29 | begin
30 | uri = URI.parse("https://secure.gaug.es/#{type}")
31 | http = Net::HTTP.new(uri.host, uri.port)
32 | http.use_ssl = true
33 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
34 |
35 | request = Net::HTTP::Get.new(uri.request_uri)
36 | request.add_field("X-Gauges-Token", "#{key}")
37 | res = http.request(request)
38 | rescue Exception => e
39 | @log.error("ERROR retrieving Gaug.es information. (#{type})")
40 | # p e
41 | end
42 |
43 | return false if res.nil?
44 | JSON.parse(res.body)
45 | end
46 |
47 | def do_log
48 | if @config.key?(self.class.name)
49 | config = @config[self.class.name]
50 | if !config.key?('gauges_token') || config['gauges_token'] == ''
51 | @log.warn("Gaug.es key has not been configured or is invalid, please edit your slogger_config file.")
52 | return
53 | end
54 | key = config['gauges_token']
55 | else
56 | @log.warn("Gaug.es key has not been configured, please edit your slogger_config file.")
57 | return
58 | end
59 | @log.info("Logging Gaug.es stats")
60 |
61 | date = @timespan + (60 * 60 * 24)
62 |
63 | json = gauges_api_call(key,"gauges")
64 | return false unless json && json.has_key?('guages')
65 | gauges = []
66 |
67 | while date.strftime("%Y%m%d") <= Time.now.strftime("%Y%m%d")
68 | json['gauges'].each {|g|
69 | gauge = {}
70 | gauge['title'] = g['title']
71 | gauge['date'] = date
72 | urls = g['urls']
73 |
74 | traffic = gauges_api_call(key,urls['traffic']+"?date=#{date.strftime("%Y-%m-%d")}")
75 |
76 | traffic['traffic'].each { |t|
77 | if t['date'] == date.strftime("%Y-%m-%d")
78 | gauge['today'] = {'views' => t['views'], 'visits' => t['people']}
79 | end
80 | }
81 |
82 | pages = gauges_api_call(key,urls['content']+"?date=#{date.strftime("%Y-%m-%d")}")
83 | referrers = gauges_api_call(key,urls['referrers']+"?date=#{date.strftime("%Y-%m-%d")}")
84 | gauge['top_pages'] = pages['content'][0..5]
85 | gauge['top_referrers'] = referrers['referrers'][0..5]
86 |
87 | gauges.push(gauge)
88 | }
89 | date = date + (60 * 60 * 24)
90 | end
91 |
92 | gauges.each {|gauge|
93 | output = ""
94 | # p date.strftime(@date_format)
95 | # p gauge['title']
96 | # p gauge['today']
97 | output += "* Visits: **#{gauge['today']['visits']}**\n"
98 | output += "* Views: **#{gauge['today']['views']}**"
99 |
100 | output += "\n\n### Top content:\n\n"
101 |
102 | gauge['top_pages'].each {|page|
103 | output += "* [#{page['title']}](#{page['url']}) (#{page['views']})\n"
104 | }
105 |
106 | output += "\n\n### Top referrers:\n\n"
107 |
108 | gauge['top_referrers'].each {|ref|
109 | output += "* <#{ref['url']}> (#{ref['views']})\n"
110 | }
111 | output += "\n\n"
112 |
113 | return false if output.strip == ""
114 | entry = "# Gaug.es report for #{gauge['title']} on #{gauge['date'].strftime(@date_format)}\n\n#{output}\n(#{config['gauges_tags']})"
115 | DayOne.new.to_dayone({ 'content' => entry, 'datestamp' => gauge['date'].utc.iso8601 })
116 | }
117 | end
118 |
119 | end
120 |
--------------------------------------------------------------------------------
/plugins_disabled/getgluelogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: GetGlue Logger
3 | Version: 1.0
4 | Description: Brief description (one line)
5 | Author: [Dom Barnes](http://dombarnes.com)
6 | Configuration:
7 | getglue_username: Used for h1 in journal entry
8 | getglue_feed: Retrieve this from your GetGlue profile page (http://getglue.com/username). You will need to view source to find this.
9 | Notes:
10 | - multi-line notes with additional description and information (optional)
11 | =end
12 |
13 | config = {
14 | 'description' => ['GetGlue logger grabs all your activity including checkins, likes and stickers',
15 | 'You will need the RSS feed of your Activity stream.'],
16 | 'getglue_username' => 'getglue',
17 | 'getglue_feed' => "",
18 | 'tags' => '#social #entertainment'
19 | }
20 |
21 | $slog.register_plugin({ 'class' => 'GetglueLogger', 'config' => config })
22 |
23 |
24 | class GetglueLogger < Slogger
25 | # every plugin must contain a do_log function which creates a new entry using the DayOne class (example below)
26 | # @config is available with all of the keys defined in "config" above
27 | # @timespan and @dayonepath are also available
28 | # returns: nothing
29 | def do_log
30 | if @config.key?(self.class.name)
31 | config = @config[self.class.name]
32 | # check for a required key to determine whether setup has been completed or not
33 | if !config.key?('getglue_username') || config['getglue_username'] == []
34 | @log.warn("GetGlue has not been configured or an option is invalid, please edit your slogger_config file.")
35 | return
36 | else
37 | # set any local variables as needed
38 | username = config['getglue_username']
39 | end
40 | else
41 | @log.warn("GetGlue has not been configured or a feed is invalid, please edit your slogger_config file.")
42 | return
43 | end
44 | @log.info("Logging GetGlue posts for #{username}")
45 | @feed = config['getglue_feed']
46 |
47 | tags = config['tags'] || ''
48 | tags = "\n\n#{@tags}\n" unless @tags == ''
49 |
50 | today = @timespan
51 |
52 | # Perform necessary functions to retrieve posts
53 | entrytext = ''
54 | rss_content = ''
55 | begin
56 | feed_url = URI.parse(@feed)
57 | feed_url.open do |f|
58 | rss_content = f.read
59 | end
60 | rescue Exception => e
61 | raise "ERROR fetching GetGlue feed"
62 | p e
63 | end
64 | content = ''
65 | rss = RSS::Parser.parse(rss_content, false)
66 | rss.items.each { |item|
67 | break if Time.parse(item.pubDate.to_s) < @timespan
68 | if item.description !=""
69 | content += "* [#{item.pubDate.strftime(@time_format)}](#{item.link}) - #{item.title} \"#{item.description}\"\n"
70 | else
71 | content += "* [#{item.pubDate.strftime(@time_format)}](#{item.link}) - #{item.title}\n"
72 | end
73 | }
74 | if content != ''
75 | entrytext = "## GetGlue Checkins for #{@timespan.strftime(@date_format)}\n\n" + content + "\n#{@tags}"
76 | end
77 |
78 | # create an options array to pass to 'to_dayone'
79 | # all options have default fallbacks, so you only need to create the options you want to specify
80 | if content != ''
81 | options = {}
82 | options['content'] = "## GetGlue Activity for #{@timespan.strftime(@date_format)}\n\n#{content} #{tags}"
83 | options['datestamp'] = @timespan.utc.iso8601
84 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
85 |
86 |
87 | # Create a journal entry
88 | # to_dayone accepts all of the above options as a hash
89 | # generates an entry base on the datestamp key or defaults to "now"
90 | sl = DayOne.new
91 | sl.to_dayone(options)
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/plugins_disabled/gistlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Gist Logger
3 | Description: Logs daily Gists for the specified user
4 | Author: [Brett Terpstra](http://brettterpstra.com)
5 | Configuration:
6 | gist_user: githubuser
7 | gist_tags: "#social #coding"
8 | Notes:
9 |
10 | =end
11 | # NOTE: Requires json gem
12 | config = {
13 | 'description' => ['Logs daily Gists for the specified user','gist_user should be your Github username'],
14 | 'gist_user' => '',
15 | 'gist_tags' => '#social #coding',
16 | }
17 | $slog.register_plugin({ 'class' => 'GistLogger', 'config' => config })
18 |
19 | class GistLogger < Slogger
20 |
21 | def do_log
22 | if @config.key?(self.class.name)
23 | config = @config[self.class.name]
24 | if !config.key?('gist_user') || config['gist_user'] == ''
25 | @log.warn("RSS feeds have not been configured or a feed is invalid, please edit your slogger_config file.")
26 | return
27 | end
28 | else
29 | @log.warn("Gist user has not been configured, please edit your slogger_config file.")
30 | return
31 | end
32 | @log.info("Logging gists for #{config['gist_user']}")
33 | begin
34 | url = URI.parse "https://api.github.com/users/#{config['gist_user']}/gists"
35 |
36 | http = Net::HTTP.new url.host, url.port
37 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
38 | http.use_ssl = true
39 |
40 | res = nil
41 |
42 | http.start do |agent|
43 | res = agent.get(url.path).read_body
44 | end
45 | rescue Exception => e
46 | raise "ERROR retrieving Gist url: #{url}"
47 | p e
48 | end
49 | # begin
50 | # gist_url = URI.parse("https://api.github.com/users/#{@user}/gists")
51 | # res = Net::HTTPS.get_response(gist_url).body
52 |
53 | return false if res.nil?
54 | json = JSON.parse(res)
55 |
56 | output = ""
57 |
58 | json.each {|gist|
59 | date = Time.parse(gist['created_at'])
60 | if date > @timespan
61 | output += "* Created [Gist ##{gist['id']}](#{gist["html_url"]})\n"
62 | output += " * #{gist["description"]}\n" unless gist["description"].nil?
63 | else
64 | break
65 | end
66 | }
67 |
68 | return false if output.strip == ""
69 | entry = "## Gists for #{Time.now.strftime(@date_format)}:\n\n#{output}\n#{config['gist_tags']}"
70 | DayOne.new.to_dayone({ 'content' => entry })
71 | end
72 |
73 | end
74 |
--------------------------------------------------------------------------------
/plugins_disabled/githubcommitlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Github Commit Logger
3 | Description: Logs daily Github commit activity(public and private) for the specified user.
4 | Author: [David Barry](https://github.com/DavidBarry)
5 | Configuration:
6 | github_user: githubuser
7 | github_token: githubtoken
8 | github_tags: "#social #coding"
9 | Notes:
10 | This requires getting an OAuth token from github to get access to your private commit activity.
11 | You can get a token by running this command in the terminal:
12 | curl -u 'username' -d '{"scopes":["repo"],"note":"Help example"}' https://api.github.com/authorizations
13 | where username is your github username.
14 | =end
15 | # NOTE: Requires json gem
16 | config = {
17 | 'description' => ['Logs daily Github commit activity(public and private) for the specified user.',
18 | 'github_user should be your Github username',
19 | 'Instructions to get Github token '],
20 | 'github_user' => '',
21 | 'github_token' => '',
22 | 'github_tags' => '#social #coding',
23 | }
24 | $slog.register_plugin({ 'class' => 'GithubCommitLogger', 'config' => config })
25 |
26 | class GithubCommitLogger < Slogger
27 |
28 | def do_log
29 | if @config.key?(self.class.name)
30 | config = @config[self.class.name]
31 | if !config.key?('github_user') || config['github_user'] == ''
32 | @log.warn("Github user has not been configured or is invalid, please edit your slogger_config file.")
33 | return
34 | end
35 |
36 | if !config.key?('github_token') || config['github_token'] == ''
37 | @log.warn("Github token has not been configured, please edit your slogger_config file.")
38 | return
39 | end
40 | else
41 | @log.warn("Github Commit Logger has not been configured, please edit your slogger_config file.")
42 | return
43 | end
44 | @log.info("Logging Github activity for #{config['github_user']}")
45 | begin
46 | url = URI.parse "https://api.github.com/users/#{config['github_user']}/events?access_token=#{config['github_token']}"
47 |
48 | res = Net::HTTP.start(url.host, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
49 | http.get url.request_uri, 'User-Agent' => 'Slogger'
50 | end
51 |
52 | rescue Exception => e
53 | @log.error("ERROR retrieving Github url: #{url}")
54 | end
55 |
56 | return false if res.nil?
57 | json = JSON.parse(res.body)
58 |
59 | output = ""
60 |
61 | json.each {|action|
62 | date = Time.parse(action['created_at'])
63 | if date > @timespan
64 | case action['type']
65 | when "PushEvent"
66 | if !action['repo']
67 | action['repo'] = {"name" => "unknown repository"}
68 | end
69 | output += "* Pushed to branch *#{action['payload']['ref'].gsub(/refs\/heads\//,'')}* of [#{action['repo']['name']}](#{action['url']})\n"
70 | action['payload']['commits'].each do |commit|
71 | output += " * #{commit['message'].gsub(/\n+/," ")}\n"
72 | end
73 | end
74 | else
75 | break
76 | end
77 | }
78 |
79 | return false if output.strip == ""
80 | entry = "Github activity for #{Time.now.strftime(@date_format)}:\n\n#{output}\n#{config['github_tags']}"
81 | DayOne.new.to_dayone({ 'content' => entry })
82 | end
83 |
84 | end
85 |
--------------------------------------------------------------------------------
/plugins_disabled/misologger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Miso Logger
3 | Description: Add the films and tv shows that you watch
4 | Author: [Alejandro Martinez](http://alejandromp.com)
5 | Configuration:
6 | miso_feed: "http://gomiso.com/feeds/user/ID/checkins.rss"
7 | pre_title: "Watched"
8 | Notes:
9 | - The miso_feed parameter is like -> http://gomiso.com/feeds/user/ID/checkins.rss
10 | - You need the change the ID with your user id. You can find your user id going to http://gomiso.com/resources/widget and watching in the code snippet.
11 | =end
12 |
13 | require 'nokogiri'
14 |
15 | config = { # description and a primary key (username, url, etc.) required
16 | 'description' => ['MisoLogger downloads your feed from Miso and add the Films and TVShows that you watch to DayOne',
17 | 'The miso_feed parameter is like -> http://gomiso.com/feeds/user/ID/checkins.rss',
18 | 'You need to change the ID with your user id. You can find your user id going to http://gomiso.com/resources/widget and watching in the code snippet.'],
19 | 'miso_feed' => "",
20 | 'pre_title' => "Watched",
21 | 'save_images' => true,
22 | 'tags' => '#social #entertainment' # A good idea to provide this with an appropriate default setting
23 | }
24 | # Update the class key to match the unique classname below
25 | $slog.register_plugin({ 'class' => 'MisoLogger', 'config' => config })
26 |
27 | # unique class name: leave '< Slogger' but change ServiceLogger (e.g. LastFMLogger)
28 | class MisoLogger < Slogger
29 | # every plugin must contain a do_log function which creates a new entry using the DayOne class (example below)
30 | # @config is available with all of the keys defined in "config" above
31 | # @timespan and @dayonepath are also available
32 | # returns: nothing
33 | def do_log
34 | if @config.key?(self.class.name)
35 | config = @config[self.class.name]
36 | # check for a required key to determine whether setup has been completed or not
37 | if !config.key?('miso_feed') || config['miso_feed'] == ''
38 | @log.warn("miso_feed has not been configured or an option is invalid, please edit your slogger_config file.")
39 | return
40 | else
41 | # set any local variables as needed
42 | feed = config['miso_feed']
43 | saveImages = config['save_images']
44 | end
45 |
46 | else
47 | @log.warn("MisoLogger has not been configured or a feed is invalid, please edit your slogger_config file.")
48 | return
49 | end
50 | @log.info("Logging MisoLogger posts for #{feed}")
51 |
52 | additional_config_option = config['additional_config_option'] || false
53 | tags = config['tags'] || ''
54 | tags = "\n\n#{tags}\n" unless @tags == ''
55 | today = @timespan
56 |
57 | ## Download Miso feed
58 | rss_content = ''
59 | begin
60 | url = URI.parse(feed)
61 |
62 | http = Net::HTTP.new url.host, url.port
63 | #http.verify_mode = OpenSSL::SSL::VERIFY_NONE
64 | #http.use_ssl = true
65 |
66 | res = nil
67 |
68 | http.start do |agent|
69 | rss_content = agent.get(url.path).read_body
70 | end
71 |
72 | rescue Exception => e
73 | @log.error("ERROR fetching Miso feed" + e.to_s)
74 | end
75 |
76 | watched = config['pre_title'] || ''
77 |
78 | ## Parse feed
79 | rss = Nokogiri::XML(rss_content)
80 | content = ''
81 | image = ''
82 | date = Time.now.utc.iso8601
83 | rss.css('item').each { |item|
84 | break if Time.parse(item.at("pubDate").text) < @timespan
85 |
86 | title = item.at("title").text
87 | description = item.at("description").text
88 | date = item.at("pubDate").text
89 | image = item.at("miso|image_url").text
90 |
91 | content += "\n" + "## " + watched + " " + title + "\n" + description
92 | }
93 |
94 | if content != ''
95 | # create an options array to pass to 'to_dayone'
96 | options = {}
97 | options['content'] = content + "\n" + "#{tags}"
98 | options['datestamp'] = Time.parse(date).utc.iso8601
99 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
100 |
101 | # Create a journal entry
102 | sl = DayOne.new
103 | if image == '' || !saveImages
104 | sl.to_dayone(options)
105 | else
106 | path = sl.save_image(image,options['uuid'])
107 | sl.store_single_photo(path,options) unless path == false
108 | end
109 | end
110 | end
111 |
112 | def helper_function(args)
113 | # add helper functions within the class to handle repetitive tasks
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/plugins_disabled/movesapplogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: MovesApp Logger
3 | Description: Proof of Concept Exporter for Moves.app (one line)
4 | Author: Martin R.J. Cleaver (http://github.com/mrjcleaver)
5 | Configuration:
6 | option_1_name: [ "example_value1" , "example_value2", ... ]
7 | option_2_name: example_value
8 | Notes:
9 | - This connects to Moves, and dumps the JSON.
10 |
11 |
12 | - It is not pretty. It's a starting point.
13 | - you need to generate an Access Token, Client ID and Client Secret
14 | - the functionality for this generation is not yet part of this logger
15 | - instead you can get these init tokens via https://github.com/pwaldhauer/elizabeth, the NodeJS MovesApp project
16 | - after which you can switch to using the MovesApp Logger
17 |
18 | Ready your slogger_config file with the MovesAppLogger section
19 | - ruby slogger --update-config
20 |
21 | Getting Your Tokens using Elizabeth's init
22 | - to generate these you need to
23 | ./ellie.js init
24 | more ~/.elizabeth.json
25 | {
26 | "moves": {
27 | "clientId": "...",
28 | "clientSecret": "....",
29 | "redirectUri": "http://localhost:3000/auth",
30 | "accessToken": "..."
31 | - now let MovesAppLogger use this by copying those values into your Slogger_config file
32 |
33 | Now you can use the MovesAppLogger
34 | -- slogger --onlyrun movesapplogger
35 |
36 | Improving the MovesAppLogger
37 | - Yup, JSON is ugly an not useful
38 | - Images would be nice
39 | - Your contribution goes here etc.
40 |
41 | =end
42 |
43 | # To your Gemfile, you'll want to add:
44 | # gem 'moves' # for movesapp
45 | # and then bundle install
46 | require 'moves'; # https://github.com/ankane/moves.git
47 |
48 | config = { # description and a primary key (username, url, etc.) required
49 | 'description' => ['Moves logger'],
50 | 'service_username' => '', # update the name and make this a string or an array if you want to handle multiple accounts.
51 | 'additional_config_option' => false,
52 | 'clientId' => '',
53 | 'clientSecret' => '',
54 | 'redirectUri' => 'http://localhost:3000/auth',
55 | 'accessToken' => '',
56 | 'tags' => '#movesapp' # A good idea to provide this with an appropriate default setting
57 | }
58 | # Update the class key to match the unique classname below
59 | $slog.register_plugin({ 'class' => 'MovesAppLogger', 'config' => config })
60 |
61 | # unique class name: leave '< Slogger' but change ServiceLogger (e.g. LastFMLogger)
62 | class MovesAppLogger < Slogger
63 | # every plugin must contain a do_log function which creates a new entry using the DayOne class (example below)
64 | # @config is available with all of the keys defined in "config" above
65 | # @timespan and @dayonepath are also available
66 | # returns: nothing
67 | def do_log
68 | if @config.key?(self.class.name)
69 | config = @config[self.class.name]
70 | # check for a required key to determine whether setup has been completed or not
71 | if !config.key?('service_username') || config['service_username'] == []
72 | @log.warn("MovesAppLogger has not been configured or an option is invalid, please edit your slogger_config file.")
73 | return
74 | else
75 | # set any local variables as needed
76 | username = config['service_username']
77 | end
78 | else
79 | @log.warn("MovesAppLogger has not been configured or a feed is invalid, please edit your slogger_config file.")
80 | return
81 | end
82 | @log.level = Logger::DEBUG
83 |
84 | if config['debug'] then ## TODO - move into the Slogger class.
85 | @log.level = Logger::DEBUG
86 | @log.debug 'Enabled debug mode'
87 | end
88 |
89 | @log.info("Logging MovesAppLogger posts from MovesApp API")
90 | @log.info config
91 |
92 | tags = config['tags'] || ''
93 | @_tags = "\n\n#{tags}\n" unless tags == ''
94 |
95 |
96 |
97 | @log.debug "Timespan formatted:"+@timespan.strftime("%l %M")
98 | last_run = config['MovesAppLogger_last_run']
99 | @current_run_time = Time.now
100 |
101 | def no_mins(t) # http://stackoverflow.com/a/4856312/722034
102 | Time.at(t.to_i - t.sec - t.min % 60 * 60)
103 | end
104 |
105 | if (@to.nil?)
106 | time_to = no_mins(@current_run_time)
107 | else
108 | time_to = Time.parse(@to)
109 | end
110 |
111 | if (@from.nil?)
112 | time_from = no_mins(Time.parse(last_run))
113 | else
114 | time_from = Time.parse(@from)
115 | end
116 |
117 | if (@to and (@from == @to))
118 | time_to = time_from + (3600 * 24 - 1)
119 | @log.debug("As from==to, assuming we mean the 24 hours starting at "+@from)
120 | end
121 |
122 | @log.debug "From #{time_from} to #{time_to}"
123 | exporter = MovesAppExporter.new(config, @log)
124 |
125 | add_blog_for_period(time_from, time_to, exporter)
126 |
127 |
128 | end
129 |
130 | def add_blog_for_period(from, to, exporter)
131 | title = "MovesApp (Auto; #{from.strftime("%l %p")}-#{to.strftime("%l %p")}; exported at #{@current_run_time.strftime("%FT%R")})"
132 |
133 | # Perform necessary functions to retrieve posts
134 | #
135 | content = exporter.getContent(from, from, to) # current_hour, or since last ran
136 |
137 | if content.nil? or content == ''
138 | @log.debug("No content = no blog post")
139 | return
140 | end
141 |
142 | one_minute_before_hour = to - 60 # Put it in at e.g. 9:59 am, so it's in the right hour
143 | blog_date_stamp = one_minute_before_hour.utc.iso8601
144 |
145 | @log.debug "Writing to datestamp "+blog_date_stamp
146 | # create an options array to pass to 'to_dayone'
147 | # all options have default fallbacks, so you only need to create the options you want to specify
148 | options = {}
149 | options['content'] = "## #{title}\n\n#{content}\n#{@_tags}"
150 | options['datestamp'] = blog_date_stamp
151 | options['starred'] = false
152 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
153 |
154 | # Create a journal entry
155 | # to_dayone accepts all of the above options as a hash
156 | # generates an entry base on the datestamp key or defaults to "now"
157 | sl = DayOne.new
158 | pp sl.to_dayone(options)
159 |
160 |
161 | # To create an image entry, use `sl.to_dayone(options) if sl.save_image(imageurl,options['uuid'])`
162 | # save_image takes an image path and a uuid that must be identical the one passed to to_dayone
163 | # save_image returns false if there's an error
164 | end
165 |
166 | end
167 |
168 |
169 | class MovesAppExporter
170 | require 'moves'
171 |
172 | def initialize(config, log)
173 | @config = config
174 | @log = log
175 | @access_token = config['accessToken']
176 | if @access_token.nil?
177 | log.error "Access Token is Not Set!"
178 | exit 1
179 | end
180 | @log.debug "Logging into Moves.app with #{@access_token}"
181 | @moves = Moves::Client.new(@access_token)
182 | end
183 |
184 |
185 |
186 | def getContent(date_from, from, to)
187 |
188 | @tzformat = "%Y-%m-%d"
189 |
190 | ## TODO
191 | # check # max 31 days period, or other Moves API constraint.
192 |
193 | from_formatted = from.strftime(@tzformat)
194 | to_formatted = to.strftime(@tzformat)
195 |
196 | #puts Time.now.utc.iso8601
197 | @log.info("FROM=#{from_formatted} TO=#{to_formatted}")
198 |
199 | @log.debug "call"
200 |
201 | result = @moves.daily_activities(:from => from_formatted, :to => to_formatted)
202 | #result = result +"\n"+ @moves.daily_summary(:from => from_formatted, :to => to_formatted)
203 | #result = result + "\n" + @moves.daily_places(:from => from_formatted, :to => to_formatted)
204 | #result = result + "\n" + @moves.daily_storyline(:from => from_formatted, :to => to_formatted)
205 | # .activity_list
206 | # track_points => true
207 | @log.debug result
208 | return result
209 | end
210 |
211 | end
212 |
213 |
214 | #MovesAppExporter.new()
215 |
--------------------------------------------------------------------------------
/plugins_disabled/omnifocus.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: OmniFocus
3 | Version: 1.3
4 | Description: Grabs completed tasks from OmniFocus
5 | Notes: omnifocus_folder_filter is an optional array of folders that should be
6 | included. If empty, all tasks will be imported. Only the immediate ancestor
7 | folder will be considered, so if you have a stucture like:
8 | - Work
9 | - Client 1
10 | - Client 2
11 | You'll have to add "Client 1" and "Client 2" - "Work" will not return anything
12 | in the Client folders, only projects and tasks directly inside the Work
13 | folder.
14 | Author: [RichSomerfield](www.richsomerfield.com) & [Patrice Brend'amour](brendamour.de)
15 | =end
16 |
17 | config = {
18 | 'omnifocus_description' => [
19 | 'Grabs completed tasks from OmniFocus',
20 | 'omnifocus_folder_filter is an optional array of folders that should be included. If left empty, all tasks will be imported.'],
21 | 'omnifocus_tags' => '#tasks',
22 | 'omnifocus_save_hashtags' => true,
23 | 'omnifocus_completed_tasks' => true,
24 | 'omnifocus_log_notes' => false,
25 | 'omnifocus_folder_filter' => [],
26 | }
27 |
28 | $slog.register_plugin({ 'class' => 'OmniFocusLogger', 'config' => config })
29 |
30 | class OmniFocusLogger < Slogger
31 | def do_log
32 | if @config.key?(self.class.name)
33 | config = @config[self.class.name]
34 | filters = config['omnifocus_folder_filter'] || []
35 | else
36 | @log.warn(" has not been configured or a feed is invalid, please edit your slogger_config file.")
37 | return
38 | end
39 | @log.info("Logging OmniFocus for completed tasks")
40 |
41 | additional_config_option = config['additional_config_option'] || false
42 | omnifocus_completed_tasks = config['omnifocus_completed_tasks'] || false
43 | log_notes = config['omnifocus_log_notes'] || false
44 | tags = config['omnifocus_tags'] || ''
45 | tags = "\n\n(#{tags})\n" unless @tags == ''
46 |
47 |
48 | output = ''
49 | developMode = $options[:develop]
50 |
51 |
52 | # Run an embedded applescript to get today's completed tasks
53 |
54 | if filters.empty? then
55 | filters = ["NONE", ]
56 | end
57 |
58 | # ============================================================
59 | # iterate over the days and create entries
60 | $i = 0
61 | days = $options[:timespan]
62 | if developMode
63 | @log.info("Running plugin for the last #{days} days")
64 | end
65 |
66 | until $i >= days do
67 | currentDate = Time.now - ((60 * 60 * 24) * $i)
68 | timestring = currentDate.strftime('%d/%m/%Y')
69 |
70 | if developMode
71 | @log.info("Running plugin for #{timestring}")
72 | end
73 |
74 | for filter in filters
75 | values = %x{osascript <<'APPLESCRIPT'
76 | set filter to "#{filter}"
77 | set dteToday to setDate("#{timestring}")
78 | tell application "OmniFocus"
79 | tell default document
80 | if filter is equal to "NONE" then
81 | set refDoneToday to a reference to (flattened tasks where (completion date ≥ dteToday))
82 | else
83 | set refDoneToday to a reference to (flattened tasks where (completion date ≥ dteToday) and name of containing project's folder = filter)
84 |
85 | end if
86 | set {lstName, lstContext, lstProject, lstNote} to {name, name of its context, name of its containing project, note} of refDoneToday
87 | set strText to ""
88 |
89 | set numberOfItems to count of lstName
90 | repeat with iTask from 1 to numberOfItems
91 | set {strName, varContext, varProject, varNote} to {item iTask of lstName, item iTask of lstContext, item iTask of lstProject, item iTask of lstNote}
92 |
93 | set contextString to "null"
94 | set projectString to "null"
95 | set noteString to "null"
96 | if varContext is not missing value then set contextString to varContext
97 | if varProject is not missing value then set projectString to varProject
98 | if varNote is not missing value then set noteString to varNote
99 |
100 | set noteString to my replaceText(noteString, linefeed, "\\\\n")
101 |
102 | set delimiterString to "##__##"
103 |
104 | set strText to strText & strName & delimiterString & projectString & delimiterString & contextString & delimiterString & noteString & linefeed
105 |
106 | end repeat
107 | end tell
108 | end tell
109 | return strText
110 |
111 | on setDate(theDateStr)
112 | set {TID, text item delimiters} to {text item delimiters, "/"}
113 | set {dd, mm, yy, text item delimiters} to every text item in theDateStr & TID
114 | set t to current date
115 | set year of t to (yy as integer)
116 | set month of t to (mm as integer)
117 | set day of t to (dd as integer)
118 | set hours of t to 0
119 | set minutes of t to 0
120 | set seconds of t to 0
121 | return t
122 | end setDate
123 |
124 | to replaceText(someText, oldItem, newItem)
125 | (*
126 | replace all occurances of oldItem with newItem
127 | parameters - someText [text]: the text containing the item(s) to change
128 | oldItem [text, list of text]: the item to be replaced
129 | newItem [text]: the item to replace with
130 | returns [text]: the text with the item(s) replaced
131 | *)
132 | set {tempTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, oldItem}
133 | try
134 | set {itemList, AppleScript's text item delimiters} to {text items of someText, newItem}
135 | set {someText, AppleScript's text item delimiters} to {itemList as text, tempTID}
136 | on error errorMessage number errorNumber -- oops
137 | set AppleScript's text item delimiters to tempTID
138 | error errorMessage number errorNumber -- pass it on
139 | end try
140 |
141 | return someText
142 | end replaceText
143 | APPLESCRIPT}
144 |
145 | unless values.strip.empty?
146 | unless filter == "NONE"
147 | output += "\n## Tasks in #{filter}\n"
148 | end
149 | tasks_completed = 0
150 | values.squeeze("\n").each_line do |value|
151 | # Create entries here
152 | tasks_completed += 1
153 | #ensures that only valid characters are saved to output
154 |
155 | #this only works in newer ruby versions but not in the default 1.8.7
156 | begin
157 | value = value.chars.select{|i| i.valid_encoding?}.join
158 | rescue
159 | end
160 |
161 | name, project, context, note = value.split("##__##")
162 |
163 | taskString = "## #{name}\n "
164 |
165 | if context != "null"
166 | taskString += "*Context:* #{context} \n"
167 | end
168 | if project != "null"
169 | taskString += "*Project:* #{project}\n"
170 | end
171 | if log_notes && note != "null" && note != "\n"
172 | note = note.gsub("\\n","\n> ")
173 | taskString += "*Notes:*\n> #{note}\n"
174 | end
175 |
176 | output += taskString
177 | end
178 | output += "\n"
179 | end
180 | end
181 | #If omnifocus_completed_tasks is true then set text for insertion
182 | if omnifocus_completed_tasks then
183 | text_completed = "#{tasks_completed} tasks completed today! \n\n"
184 | end
185 |
186 | # Create a journal entry
187 | unless output == ''
188 | options = {}
189 | options['content'] = "# OmniFocus - Completed Tasks\n\n#{text_completed}#{output}#{tags}"
190 | sl = DayOne.new
191 | sl.to_dayone(options)
192 | end
193 | $i += 1
194 | end
195 | return config
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/plugins_disabled/pocketlogger_api.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Pocket Logger
3 | Description: Logs today's additions to Pocket.
4 | Notes:
5 | pocket_username is a string with your Pocket username
6 | Author: [Brett Terpstra](http://brettterpstra.com)
7 | Configuration:
8 | pocket_username: "your_username"
9 | pocket_passwd: "your_password"
10 | pocket_tags: "#social #reading"
11 | posts_to_get: "read" or "unread" or leave blank for all
12 | Notes:
13 |
14 | =end
15 | config = {
16 | 'pocket_description' => [
17 | 'Logs today\'s posts to Pocket.',
18 | 'pocket_username is a string with your Pocket username',
19 | 'pocket_passwd is a string with your Pocket password',
20 | 'pocket_tags are the tags you want assigned to each dayone entry',
21 | 'posts_to_get allows you to choose read, unread or all items'],
22 | 'pocket_username' => '',
23 | 'pocket_passwd' => '',
24 | 'pocket_tags' => '#social #reading',
25 | 'posts_to_get' => ''
26 | }
27 | $slog.register_plugin({ 'class' => 'PocketLogger', 'config' => config })
28 |
29 | require 'rexml/document'
30 | require 'oauth'
31 | #require 'ruby-debug'
32 |
33 | class PocketLogger < Slogger
34 | #Debugger.start
35 | def do_log
36 | if @config.key?(self.class.name)
37 | config = @config[self.class.name]
38 | if !config.key?('pocket_username') || config['pocket_username'].nil? || !config.key?('pocket_passwd') || config['pocket_passwd'].nil?
39 | @log.warn("Pocket username has not been configured, please edit your slogger_config file.")
40 | return
41 | end
42 | else
43 | @log.warn("Pocket has not been configured, please edit your slogger_config file.")
44 | return
45 | end
46 |
47 | sl = DayOne.new
48 | config['pocket_tags'] ||= ''
49 | username = config['pocket_username']
50 | passwd= config['pocket_passwd']
51 | posts_to_get=config['posts_to_get']
52 | tags = "\n\n#{config['pocket_tags']}\n" unless config['pocket_tags'] == ''
53 | today = @timespan
54 | yest=(Time.now-86400).to_i
55 | pkey="29ed8r79To6fuG8e9bA480GD77g5P586"
56 | @log.info("Getting Pocket #{posts_to_get} posts for #{username}")
57 | output = ''
58 | burl="https://readitlaterlist.com/v2/get?username=#{username}&password=#{passwd}&state=#{posts_to_get}&since=#{yest}&apikey=#{pkey}"
59 | curl=URI.parse(burl)
60 |
61 | begin
62 | res=Net::HTTP.start(curl.host) { |http| http.get("#{curl.path}?#{curl.query}") }
63 | entries=JSON.parse(res.body)
64 | entries["list"].each do | k, v|
65 | output+="#{v["title"]} // #{v["url"]} \n\n "
66 | end
67 | rescue Exception => e
68 | puts "Error getting #{posts_to_get} posts for #{username}".gsub!(" "," ")
69 | p e
70 | return ''
71 | end
72 | unless output == ''
73 | options = {}
74 | options['content'] = "Pocket reading\n\n#{output}#{tags}"
75 | sl.to_dayone(options)
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/plugins_disabled/rdiologger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Rdio Logger
3 | Description: Logs summary of activity on Rdio for the specified user
4 | Author: [Julien Grimault](github.com/juliengrimault)
5 | Configuration:
6 | rdio_username: juliengrimault
7 | Notes:
8 | - multi-line notes with additional description and information (optional)
9 | =end
10 |
11 | config = {
12 | 'description' => ['Logs tracks/albums added to your rdio collection.', 'rdio_username should be the Rdio username. include_album_image determines wether the album image is included in the journal entry'],
13 | 'rdio_username' => '',
14 | 'include_album_image' => true,
15 | 'tags' => '#social #music'
16 | }
17 | # Update the class key to match the unique classname below
18 | $slog.register_plugin({ 'class' => 'RdioLogger', 'config' => config })
19 |
20 | require 'rdio_api'
21 | class RdioLogger < Slogger
22 | RDIO_LOGGER_TABLE_WIDTH = 3
23 | # every plugin must contain a do_log function which creates a new entry using the DayOne class (example below)
24 | # @config is available with all of the keys defined in "config" above
25 | # @timespan and @dayonepath are also available
26 | # returns: nothing
27 | def do_log
28 |
29 | unless logger_registered?
30 | @log.warn("Rdio logger was not registered, please edit your slogger_config file.")
31 | return
32 | end
33 |
34 | unless logger_configured?
35 | @log.warn("Rdio user has not been configured or an option is invalid, please edit your slogger_config file.")
36 | return
37 | end
38 |
39 | @log.info("Logging Rdio activity for #{logger_config['rdio_username']}")
40 |
41 |
42 | userKey = try { next get_user_key() }
43 | return nil unless userKey
44 |
45 | activities = try { next get_activities(userKey) }
46 | return nil unless activities && activities.count > 0
47 |
48 | albums = get_albums(activities)
49 | content = generate_content(albums)
50 |
51 | sl = DayOne.new
52 | sl.to_dayone({ 'content' => "Rdio Activity - Album#{albums.count > 1 ? "s" : ""} added to collection\n#{content}\n\n#{tags}"})
53 | end
54 |
55 | private
56 | def logger_registered?
57 | @config.key?(self.class.name)
58 | end
59 |
60 | def logger_configured?
61 | logger_config.key?('rdio_username') && logger_config['rdio_username'] != ''
62 | end
63 |
64 | def logger_config
65 | @config[self.class.name]
66 | end
67 |
68 | def rdio
69 | @rdio ||= RdioApi.new(:consumer_key => 'xxh3fr2p2s9xu9ps4b7gj888', :consumer_secret => 'ckwHAXrAkK')
70 | end
71 |
72 | def tags
73 | logger_config['tags'] || ''
74 | end
75 |
76 | def try(&action)
77 | retries = 0
78 | success = false
79 | until success || $options[:max_retries] == retries
80 | begin
81 | result = yield
82 | success = true
83 | rescue => e
84 | @log.error e
85 | retries += 1
86 | @log.error("Error performing action, retrying (#{retries}/#{$options[:max_retries]})")
87 | sleep 2
88 | end
89 | end
90 | result
91 | end
92 |
93 | def get_user_key
94 | user = rdio.findUser(:vanityName => logger_config['rdio_username'])
95 | return nil unless user
96 | user['key']
97 | end
98 |
99 | def get_activities(userKey)
100 | response = rdio.getActivityStream(:user => userKey, :scope => "user")
101 | return nil unless response
102 | response['updates'].select { |item| is_activity_valid?(item) }
103 | end
104 |
105 | def is_activity_valid?(activity)
106 | activity['update_type'] == 0 && Time.parse(activity['date']) > @timespan
107 | end
108 |
109 | def get_albums(activities)
110 | activities.reduce([]) { |result, activity| result.concat(activity['albums']) }
111 | end
112 |
113 | def generate_content(albums)
114 | if logger_config['include_album_image']
115 | generate_table_content(albums)
116 | else
117 | generate_text_content(albums)
118 | end
119 | end
120 |
121 | def generate_table_content(albums)
122 | result = ""
123 | albums.each_with_index do |album, i|
124 | result += generate_entry_with_image(album) + table_separator(i)
125 | end
126 | result
127 | end
128 |
129 | def table_separator(index)
130 | if end_of_row?(index)
131 | seperator = "\n"
132 | if end_of_first_row?(index)
133 | seperator += table_header_md + "\n"
134 | end
135 | else
136 | seperator = " | "
137 | end
138 | seperator
139 | end
140 |
141 | def end_of_row?(index)
142 | (index + 1) % RDIO_LOGGER_TABLE_WIDTH == 0
143 | end
144 |
145 | def end_of_first_row?(index)
146 | (index + 1) == RDIO_LOGGER_TABLE_WIDTH
147 | end
148 |
149 | def table_header_md
150 | Array.new(RDIO_LOGGER_TABLE_WIDTH, ":-------:").join(" | ")
151 | end
152 |
153 | def generate_entry_with_image(album)
154 | link_text = "#{album['artist']} - #{album['name']}"
155 | link_text.gsub!(/[()]/, "-") #replace parentheses with - otherwise it conflict with the md
156 |
157 | if link_text.length > 50 #limit the length of the text in the table otherwise the layout is not balanced
158 | link_text = link_text[0..50] + "..."
159 | end
160 |
161 | url = album['shortUrl']
162 | "#{md_link(link_text, url)}"
163 | end
164 |
165 | def generate_text_content(albums)
166 | albums.reduce("") { |result, album| result + generate_entry_with_text(album) }
167 | end
168 |
169 | def generate_entry_with_text(album)
170 | link_text = "#{album['artist']} - #{album['name']}"
171 | url = album['shortUrl']
172 | md_link(link_text,url)
173 | end
174 |
175 | def md_link(text, url)
176 | "[#{text}](#{url})"
177 | end
178 | end
179 |
--------------------------------------------------------------------------------
/plugins_disabled/readability_api.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Readability Logger
3 | Description: Logs today's additions to Readability.
4 | Author: [Joseph Scavone](http://scav1.com)
5 | Notes:
6 | read_username is a string with your Readability username
7 | read_passwd is a string with your Readability password
8 | read_key is a string with your Readability API key
9 | read_secret is a string with your Readability API secret
10 | Configuration:
11 | read_username: "your_username"
12 | read_passwd: "your_password"
13 | read_key: "your_key"
14 | read_secret: "your_secret"
15 | read_tags: "#social #reading"
16 | favorites_only: true|false
17 | Notes:
18 |
19 | =end
20 | config = {
21 | 'read_description' => [
22 | 'Logs today\'s posts to Readability.',
23 | 'read_username is a string with your Readability username',
24 | 'read_passwd is a string with your Readability password',
25 | 'read_key is a string with your Readability API key',
26 | 'read_secret is a string with your Readability API secret',
27 | 'favorites_only is a boolean to only return favorites'],
28 | 'read_username' => nil,
29 | 'read_passwd' => nil,
30 | 'read_key' => nil,
31 | 'read_secret' => nil,
32 | 'read_tags' => '#social #reading',
33 | 'favorites_only' => false
34 | }
35 | $slog.register_plugin({ 'class' => 'ReadabilityLogger', 'config' => config })
36 |
37 | require 'rubygems'
38 | require 'oauth'
39 |
40 | class ReadabilityLogger < Slogger
41 | def do_log
42 | if @config.key?(self.class.name)
43 | config = @config[self.class.name]
44 | if !config.key?('read_username') || config['read_username'].nil? || !config.key?('read_passwd') || config['read_passwd'].nil?
45 | @log.warn("Readability username has not been configured, please edit your slogger_config file.")
46 | return
47 | end
48 | if !config.key?('read_key') || config['read_key'].nil? || !config.key?('read_secret') || config['read_secret'].nil?
49 | @log.warn("Readability API has not been configured, please edit your slogger_config file.")
50 | return
51 | end
52 | else
53 | @log.warn("Readability has not been configured, please edit your slogger_config file.")
54 | return
55 | end
56 |
57 | sl = DayOne.new
58 | config['read_tags'] ||= ''
59 | username = config['read_username']
60 | passwd = config['read_passwd']
61 | consumer_key = config['read_key']
62 | consumer_secret = config['read_secret']
63 | favorites_only=config['favorites_only'] ? 1 : 0
64 | tags = "\n\n#{config['read_tags']}\n" unless config['read_tags'] == ''
65 | yest = @timespan.strftime("%Y-%m-%d")
66 | @log.info("Getting Readability posts for #{username}")
67 | output = ''
68 |
69 | begin
70 | consumer = OAuth::Consumer.new(consumer_key, consumer_secret,
71 | :site => "https://www.readability.com",
72 | :access_token_path => '/api/rest/v1/oauth/access_token/')
73 | access_token = consumer.get_access_token(nil, {}, {
74 | 'x_auth_mode' => 'client_auth',
75 | 'x_auth_username' => username,
76 | 'x_auth_password' => passwd})
77 | rescue OAuth::Unauthorized => e
78 | @log.error("Error with Readability API key/secret: #{e}")
79 | end
80 |
81 | unless access_token == nil
82 | begin
83 | burl = "/api/rest/v1/bookmarks/?archive=0&added_since=#{yest}&favorite=#{favorites_only}"
84 | res = access_token.get(burl)
85 | entries=JSON.parse(res.body)
86 | entries["bookmarks"].each do |item|
87 | output+="[#{item["article"]["title"]}](https://www.readability.com/articles/#{item["article"]["id"]})\n>#{item["article"]["excerpt"]}\n\n"
88 | end
89 | rescue Exception => e
90 | @log.error("Error getting reading list for #{username}: #{e}")
91 | return ''
92 | end
93 | unless output == ''
94 | options = {}
95 | options['content'] = "Readability reading\n\n#{output}#{tags}"
96 | sl.to_dayone(options)
97 | end
98 | end
99 | end
100 | end
--------------------------------------------------------------------------------
/plugins_disabled/reporterlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Reporter logger
3 | Description: Parses log files created by the reporter app for iPhone (http://www.reporter-app.com),
4 | an app that asks you questions throughout the day. The logger will create a single entry for all entries of each day.
5 | Notes: Inside the reporter app itself you will need to enable Save to Dropbox in the export settings.
6 | This logger also doesn't try to parse all the data from the app, but instead focusing on the main information.
7 | Author: [Arjen Schwarz](https://github.com/ArjenSchwarz)
8 | Configuration:
9 | reporter_source_directory: "/path/to/dropbox/Apps/Reporter-App"
10 | reporter_all_entries: true/false (This will make it run on all reporter files in the source directory. Resets to false after use)
11 | reporter_star: true/false
12 | reporter_tags: "#reporter"
13 | reporter_use_fahrenheit: true/false (Default is false, which makes it use Celcius)
14 | =end
15 |
16 | config = {
17 | 'description' => ['Parses log files created by the reporter app for iPhone'],
18 | 'reporter_source_directory' => '',
19 | 'reporter_all_entries' => false,
20 | 'reporter_star' => false,
21 | 'reporter_tags' => '',
22 | 'reporter_use_fahrenheit' => false
23 | }
24 |
25 | $slog.register_plugin({ 'class' => 'ReporterLogger', 'config' => config })
26 |
27 | class ReporterLogger < Slogger
28 | require 'date'
29 | require 'time'
30 |
31 | def do_log
32 | if @config.key?(self.class.name)
33 | config = @config[self.class.name]
34 | if !config.key?('reporter_source_directory') || config['reporter_source_directory'] == ""
35 | @log.warn("ReporterLogger has not been configured or an option is invalid, please edit your slogger_config file.")
36 | return
37 | end
38 | else
39 | @log.warn("ReporterLogger has not been configured, please edit your slogger_config file.")
40 | return
41 | end
42 | developMode = $options['develop']
43 | @tags = config['reporter_tags'] || ''
44 | tags = "\n\n#{@tags}\n" unless @tags == ''
45 |
46 | filelist = get_filelist(config)
47 |
48 | filelist.each do |inputFile|
49 | options = {}
50 | options['starred'] = config['reporter_star']
51 |
52 | f = File.new(File.expand_path(inputFile))
53 | content = JSON.parse(f.read)
54 | f.close
55 |
56 | nr_entries = content['snapshots'].count
57 |
58 | snapshots = Array.new()
59 | if nr_entries > 0
60 | content['snapshots'].each do |snapshot|
61 | snapshot_date = DateTime.parse(snapshot['date'])
62 | snapshot_text = sprintf("\n## %s\n", snapshot_date.strftime(@time_format))
63 | snapshot_text += get_location(snapshot['location'])
64 | snapshot_text += get_weather(snapshot['weather'], config['reporter_use_fahrenheit'])
65 | if snapshot.has_key? 'steps'
66 | snapshot_text += sprintf("* Steps taken: %s\n", snapshot['steps'])
67 | end
68 | if snapshot.has_key? 'photoSet'
69 | snapshot_text += sprintf("* Photos taken: %s\n", snapshot['photoSet']['photos'].count)
70 | end
71 | snapshot_text += get_responses(snapshot['responses'])
72 | snapshots.push(snapshot_text)
73 | # Set the logging timestamp to the time of the last snapshot
74 | # has to be in UTC and following the Day One required format
75 | options['datestamp'] = snapshot_date.new_offset(0).strftime('%FT%TZ')
76 | end
77 | options['content'] = sprintf("# Reporter\n\n%s\n\n%s", snapshots.join("\n---\n"), tags)
78 | sl = DayOne.new
79 | sl.to_dayone(options)
80 | end
81 | end
82 | # Ensure all entries is disabled after 1 run
83 | config['reporter_all_entries'] = false
84 | return config
85 | end
86 |
87 | # get the list of files that need to be parsed
88 | def get_filelist(config)
89 | inputDir = config['reporter_source_directory']
90 | if config['reporter_all_entries']
91 | Dir.chdir(inputDir)
92 | filelist = Dir.glob("*reporter-export.json")
93 | else
94 | days = $options[:timespan]
95 | $i = 0
96 | filelist = Array.new()
97 | until $i >= days do
98 | currentDate = Time.now - ((60 * 60 * 24) * $i)
99 | date = currentDate.strftime('%Y-%m-%d')
100 | filename = "#{inputDir}/#{date}-reporter-export.json"
101 | if File.exists?(filename)
102 | filelist.push(filename)
103 | end
104 | $i += 1
105 | end
106 | end
107 | return filelist
108 | end
109 |
110 | # Parse the location data
111 | def get_location(location)
112 | if !location.nil? && location.has_key?('placemark') && location['placemark'].has_key?('name')
113 | placemark = location['placemark']
114 | location = [placemark['name'], placemark['locality'], placemark['country']].join(', ')
115 | return sprintf("* Location: %s\n", location)
116 | else
117 | return ""
118 | end
119 | end
120 |
121 | # Parse the weather data
122 | def get_weather(weather, fahrenheit)
123 | if weather.nil?
124 | return ""
125 | end
126 | temperature = fahrenheit == true ? weather['tempF'] : weather['tempC']
127 | return sprintf("* Weather: %s (%.1f degrees)\n", weather['weather'], temperature)
128 | end
129 |
130 | # Parse the different types of responses
131 | def get_responses(responses)
132 | text = ''
133 | responses.each do |response|
134 | if response.has_key? 'textResponses'
135 | response_text = get_textresponse(response['textResponses'])
136 | elsif response.has_key? 'tokens'
137 | response_text = get_textresponse(response['tokens'])
138 | elsif response.has_key? 'numericResponse'
139 | response_text = response['numericResponse']
140 | elsif response.has_key? 'locationResponse'
141 | response_text = response['locationResponse']['text']
142 | elsif response.has_key? 'answeredOptions'
143 | response_text = response['answeredOptions'].join(", ")
144 | end
145 | text += sprintf("\n**%s**\n%s\n", response['questionPrompt'], response_text)
146 | end
147 | return text
148 | end
149 |
150 | # Collate possible multiple responses into a single text
151 | def get_textresponse(responses)
152 | response_list = Array.new()
153 | responses.each do |response|
154 | response_list.push(response['text'])
155 | end
156 | return response_list.join("\n")
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/plugins_disabled/runkeeper.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Runkeeper
3 | Description: Gets recent runkeeper data
4 | Author: Alan Schussman
5 |
6 | Notes:
7 | To run this plugin you need a Runkeeper API app ID and secret key, plus a user access_token that gets specified in the config. Some instructions for starting up with the Runkeeper API are at https://gist.github.com/ats/5538092. Structure is based heavily on Patrice Brend'amour's fitbit plugin. Provide a filename in runkeeper_save_dat_file to optionally dump the retrieved activity data into a tab-separated text file for playing with later.
8 | Configuration:
9 | runkeeper_access_token
10 | runkeeper_tags: '#activities #workout #runkeeper'
11 | runkeeper_save_data_file: '/home/users/username/data/runkeeper.txt'
12 | metric_distance: false
13 |
14 | =end
15 |
16 |
17 | config = {
18 | 'runkeeper_description' => [
19 | 'Gets runkeeper activity information'],
20 | 'runkeeper_access_token' => '',
21 | 'runkeeper_tags' => '#activities #workout #runkeeper',
22 | 'runkeeper_save_data_file' => '',
23 | 'metric_distance' => false,
24 | }
25 |
26 | $slog.register_plugin({ 'class' => 'RunkeeperLogger', 'config' => config })
27 |
28 | require 'rubygems'
29 | require 'time'
30 | require 'json'
31 |
32 | class RunkeeperLogger < Slogger
33 | def do_log
34 | if @config.key?(self.class.name)
35 | config = @config[self.class.name]
36 |
37 | # Check that the user has configured the plugin
38 | if config['runkeeper_access_token'] == ""
39 | @log.warn("Runkeeper has not been configured; you need a developer API key to create a user access_token.")
40 | return
41 | end
42 | else
43 | @log.warn("Runkeeper has not been configured; please edit your slogger_config file.")
44 | return
45 | end
46 |
47 | rk_token = config['runkeeper_access_token']
48 | save_data_file = config['runkeeper_save_data_file']
49 | metric_value = config['metric_distance']
50 | developMode = $options[:develop]
51 |
52 |
53 | # get activities array:
54 | # This is currently limited and get the most recent 25 entries,
55 | # then identifies entries in the specified days range to include
56 | # in the Day One journal entries.
57 |
58 | activitiesReq = sprintf('curl https://api.runkeeper.com/fitnessActivities -s -X GET -H "Authorization: Bearer %s"', rk_token)
59 | activities = JSON.parse(`#{activitiesReq}`)
60 |
61 | # ============================================================
62 | # iterate over the days and create entries
63 | # All based on the fitbit plugin
64 | $i = 0
65 | days = $options[:timespan]
66 | until $i >= days do
67 | currentDate = Time.now - ((60 * 60 * 24) * $i)
68 | timestring = currentDate.strftime('%F')
69 |
70 | @log.info("Logging Runkeeper summary for #{timestring}")
71 |
72 | output = ""
73 | activities["items"].each do | activity |
74 | if Date.parse(activity["start_time"]).to_s == timestring # activity is in date range
75 | activityReq = sprintf('curl https://api.runkeeper.com%s -s -X GET -H "Authorization: Bearer %s"', activity["uri"], rk_token)
76 | active = JSON.parse(`#{activityReq}`)
77 | type = active["type"]
78 | if(!metric_value)
79 | distance = (active["total_distance"]/1609.34*100).round / 100.0
80 | else
81 | distance = (active["total_distance"]/10).round / 100.0
82 | end
83 | duration = (active["duration"]/60*100).round / 100
84 | time = active["start_time"]
85 | notes = active["notes"]
86 | equipment = active["equipment"]
87 | if developMode
88 | @log.info
89 | @log.info("#{type}")
90 | @log.info("#{distance}")
91 | @log.info("#{duration}")
92 | @log.info("#{time}")
93 | @log.info("#{notes}")
94 | @log.info("#{equipment}")
95 | end
96 | output = output + "\n\n### Activity: #{type}\n* **Time**: #{time}\n"
97 | if(!metric_value)
98 | output = output + "* **Distance**: #{distance} miles\n"
99 | else
100 | output = output + "* **Distance**: #{distance} kilometers\n"
101 | end
102 | output = output + "* **Duration**: #{duration} minutes\n"
103 | output = output + "* **Equipment**: #{equipment}\n" unless equipment == "None"
104 | output = output + "* **Notes**: #{notes}\n" unless notes.nil?
105 |
106 | # save to text file if desired for stats and stuff
107 | if save_data_file != ""
108 | open(save_data_file, 'a') { |f|
109 | f.puts("#{type}\t#{distance}\t#{duration}\t#{time}\t#{equipment}")
110 | }
111 | end
112 | end
113 | end
114 | # Create a journal entry
115 | tags = config['runkeeper_tags'] || ''
116 | tags = "\n\n#{tags}\n" unless tags == ''
117 |
118 | options = {}
119 | options['content'] = "## Workouts and Exercise\n\n#{output}#{tags}"
120 | options['datestamp'] = currentDate.utc.iso8601
121 |
122 | sl = DayOne.new
123 | sl.to_dayone(options) unless output == ""
124 |
125 | $i += 1
126 | end
127 | return config
128 | end
129 | end
--------------------------------------------------------------------------------
/plugins_disabled/soundcloudlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: SoundCloud Logger
3 | Version: 1.0
4 | Description: Logs SoundCloud uploads as a digest
5 | Author: [Brett Terpstra](http://brettterpstra.com)
6 | Configuration:
7 | soundcloud_id: 20678639
8 | soundcloud_starred: false
9 | soundcloud_tags: "#social #music"
10 | Notes:
11 | - soundcloud_id is a string of numbers representing your user ID.
12 | - There may be an easier way to find this, but you can go to your Dashboard -> Tracks,
13 | - view the page source in your browser and search for "trackOwnerId"
14 | - soundcloud_starred is true or false, determines whether SoundCloud uploads are starred entries
15 | - soundcloud_tags are tags you want to add to every SoundCloud entry, e.g. "#social #music"
16 | =end
17 |
18 | config = {
19 | 'description' => ['Logs SoundCloud uploads as a digest',
20 | 'soundcloud_id is a string of numbers representing your user ID',
21 | 'Dashboard -> Tracks, view page source and search for "trackOwnerId"',
22 | 'soundcloud_starred is true or false, determines whether SoundCloud uploads are starred entries',
23 | 'soundcloud_tags are tags you want to add to every SoundCloud entry, e.g. "#social #music"'],
24 | 'soundcloud_id' => '',
25 | 'soundcloud_starred' => false,
26 | 'soundcloud_tags' => '#social #music'
27 | }
28 | $slog.register_plugin({ 'class' => 'SoundCloudLogger', 'config' => config })
29 |
30 | class SoundCloudLogger < Slogger
31 | def do_log
32 | if @config.key?(self.class.name)
33 | @scconfig = @config[self.class.name]
34 | if !@scconfig.key?('soundcloud_id') || @scconfig['soundcloud_id'] == [] || @scconfig['soundcloud_id'].nil?
35 | @log.warn("SoundCloud logging has not been configured or a feed is invalid, please edit your slogger_config file.")
36 | return
37 | else
38 | user = @scconfig['soundcloud_id']
39 | end
40 | else
41 | @log.warn("SoundCloud logging not been configured or a feed is invalid, please edit your slogger_config file.")
42 | return
43 | end
44 | @log.info("Logging SoundCloud uploads")
45 |
46 | retries = 0
47 | success = false
48 |
49 | until success
50 | if parse_feed("http://api.soundcloud.com/users/#{user}/tracks?limit=25&offset=0&linked_partitioning=1&secret_token=&client_id=ab472b80bdf8389dd6f607a10abfe33b&format=xml")
51 | success = true
52 | else
53 | break if $options[:max_retries] == retries
54 | retries += 1
55 | @log.error("Error parsing SoundCloud feed for user #{user}, retrying (#{retries}/#{$options[:max_retries]})")
56 | sleep 2
57 | end
58 | end
59 |
60 | unless success
61 | @log.fatal("Could not parse SoundCloud feed for user #{user}")
62 | end
63 |
64 | end
65 |
66 | def parse_feed(rss_feed)
67 | tags = @scconfig['soundcloud_tags'] || ''
68 | tags = "\n\n(#{tags})\n" unless tags == ''
69 | starred = @scconfig['soundcloud_starred'] || false
70 |
71 | begin
72 | rss_content = ""
73 |
74 | feed_download_response = Net::HTTP.get_response(URI.parse(rss_feed));
75 | xml_data = feed_download_response.body;
76 |
77 | doc = REXML::Document.new(xml_data);
78 | # Useful SoundCloud XML elements
79 | # created-at
80 | # permalink-url
81 | # artwork-url
82 | # title
83 | # description
84 | content = ''
85 | doc.root.each_element('//track') { |item|
86 | item_date = Time.parse(item.elements['created-at'].text)
87 | if item_date > @timespan
88 | content += "* [#{item.elements['title'].text}](#{item.elements['permalink-url'].text})\n" rescue ''
89 | desc = item.elements['description'].text
90 | content += "\n #{desc}\n" unless desc.nil? or desc == ''
91 | else
92 | break
93 | end
94 | }
95 | unless content == ''
96 | options = {}
97 | options['content'] = "## SoundCloud uploads\n\n#{content}#{tags}"
98 | options['starred'] = starred
99 | sl = DayOne.new
100 | sl.to_dayone(options)
101 | end
102 | rescue Exception => e
103 | p e
104 | return false
105 | end
106 | return true
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/plugins_disabled/stravalogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Strava Logger
3 | Description: Creates separate entries for rides and runs you finished today
4 | Author: [Patrick Walsh](http://twitter.com/zmre)
5 | Configuration:
6 | strava_access_token: "your access token"
7 | strava_tags: "#social #sports"
8 | strava_unit "metric" || "imperial"
9 | Notes:
10 | - strava_access_token is an oauth access token for your account. You can obtain one at https://www.strava.com/settings/api
11 | - strava_tags are tags you want to add to every entry, e.g. "#social #sports #cycling #training"
12 | - strava_units determine what units to display data in: "metric" or "imperial"
13 | =end
14 |
15 | require 'open-uri'
16 | require 'json'
17 |
18 | config = {
19 | 'description' => ['strava_access_token is an oauth access token for your account. You can obtain one at https://www.strava.com/settings/api',
20 | 'strava_tags are tags you want to add to every entry, e.g. "#social #sports #cycling #training"',
21 | 'strava_units determine what units to display data in: "metric" or "imperial"'],
22 | 'strava_access_token' => '',
23 | 'strava_tags' => '#social #sports',
24 | 'strava_unit' => 'metric'
25 | }
26 |
27 | $slog.register_plugin({ 'class' => 'StravaLogger', 'config' => config })
28 |
29 | class StravaLogger < Slogger
30 | NOT_CONFIGURED = 'Strava has not been configured or is invalid, please edit your slogger_config file.'
31 | NO_ACCESS_TOKEN = 'Strava access token has not been configured, please edit your slogger_config file.'
32 | def do_log
33 | @grconfig = @config[self.class.name]
34 | return @log.warn(NOT_CONFIGURED) if @grconfig.nil?
35 |
36 | access_token = @grconfig['strava_access_token']
37 | return @log.warn(NO_ACCESS_TOKEN) if access_token.nil? || access_token.strip.empty?
38 |
39 | feed = "https://www.strava.com/api/v3/athlete/activities?access_token=#{access_token}"
40 |
41 | @log.info("Logging activities from Strava")
42 |
43 | retries = 0
44 | success = false
45 |
46 | until success
47 | if parse_feed(feed)
48 | success = true
49 | else
50 | break if $options[:max_retries] == retries
51 | retries += 1
52 | @log.error("Error parsing Strava feed, retrying (#{retries}/#{$options[:max_retries]})")
53 | sleep 2
54 | end
55 |
56 | unless success
57 | @log.fatal("Could not parse feed #{feed}")
58 | end
59 | end
60 | end
61 |
62 | def parse_feed(rss_feed)
63 | tags = @grconfig['strava_tags'] || ''
64 | tags = "\n\n#{tags}\n" unless tags == ''
65 |
66 | begin
67 | res = URI.parse(rss_feed).read
68 | rescue Exception => e
69 | raise "ERROR retrieving Strava activity list url: #{rss_feed} - #{e}"
70 | end
71 |
72 | return false if res.nil?
73 |
74 | begin
75 | JSON.parse(res).each {|activity|
76 | @log.info("Examining activity #{activity['id']}: #{activity['name']}")
77 |
78 | date = Time.parse(activity['start_date_local'])
79 |
80 | if date > @timespan
81 | moving_time = Integer(activity['moving_time'])
82 | moving_time_minutes, moving_time_seconds = moving_time.divmod(60)
83 | moving_time_hours, moving_time_minutes = moving_time_minutes.divmod(60)
84 | elapsed_time = Integer(activity['elapsed_time'])
85 | elapsed_time_minutes, elapsed_time_seconds = elapsed_time.divmod(60)
86 | elapsed_time_hours, elapsed_time_minutes = elapsed_time_minutes.divmod(60)
87 |
88 | if @grconfig['strava_unit'] == 'imperial'
89 | unit = ['ft', 'mi', 'mph']
90 | activity['distance'] *= 0.000621371 #mi
91 | activity['average_speed'] *= 2.23694 #mph
92 | activity['max_speed'] *= 2.23694 #mph
93 | activity['total_elevation_gain'] *= 3.28084 #ft
94 | else
95 | unit = ['m', 'km', 'kph']
96 | activity['distance'] *= 0.001001535 #km
97 | activity['average_speed'] *= 3.611940299 #kph
98 | activity['max_speed'] *= 3.611940299 #kph
99 | end
100 |
101 | output = ''
102 | output += "# Strava Activity - %.2f %s - %dh %dm %ds - %.1f %s - %s\n\n" % [activity['distance'], unit[1], moving_time_hours, moving_time_minutes, moving_time_seconds, activity['average_speed'], unit[2], activity['name']] unless activity['name'].nil?
103 | output += "* **Description**: #{activity['description']}\n" unless activity['description'].nil?
104 | output += "* **Type**: #{activity['type']}\n" unless activity['type'].nil?
105 | output += "* **Distance**: %.2f %s\n" % [activity['distance'], unit[1]] unless activity['distance'].nil?
106 | output += "* **Elevation Gain**: %d %s\n" % [activity['total_elevation_gain'], unit[0]] unless activity['total_elevation_gain'].nil?
107 | output += "* **Average Speed**: %.1f %s\n" % [activity['average_speed'], unit[2]] unless activity['average_speed'].nil?
108 | output += "* **Max Speed**: %.1f %s\n" % [activity['max_speed'], unit[2]] unless activity['max_speed'].nil?
109 | #TODO: turn location into a Day One location
110 | output += "* **Location**: #{activity['location_city']}\n" unless activity['location_city'].nil?
111 | output += "* **Elapsed Time**: %02d:%02d:%02d\n" % [elapsed_time_hours, elapsed_time_minutes, elapsed_time_seconds] unless activity['elapsed_time'].nil?
112 | output += "* **Moving Time**: %02d:%02d:%02d\n" % [moving_time_hours, moving_time_minutes, moving_time_seconds] unless activity['moving_time'].nil?
113 | output += "* **Link**: http://www.strava.com/activities/#{activity['id']}\n"
114 |
115 | options = {}
116 | options['content'] = "#{output}#{tags}"
117 | options['datestamp'] = Time.parse(activity['start_date']).iso8601
118 | options['starred'] = false
119 | options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
120 |
121 | DayOne.new.to_dayone(options)
122 | else
123 | break
124 | end
125 | }
126 | rescue Exception => e
127 | @log.error("ERROR parsing Strava results from #{rss_feed}")
128 | raise e
129 | end
130 |
131 | return true
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/plugins_disabled/things.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Things
3 | Description: Grabs completed tasks from Things
4 | Notes: Thanks goes to RichSomerfield for the OmniFocus plugin, I used it as inspiration.
5 | things_project_filter is an optional string of a project name that should not be imported (e.g. my grocery list). If left empty, all tasks will be imported.
6 | Author: [Brian Stearns](twitter.com/brs), Patrice Brend'amour
7 | =end
8 |
9 | config = {
10 | 'things_description' => [
11 | 'Grabs completed tasks from Things',
12 | 'things_project_filter is an optional string of a project name that should not be imported (e.g. my grocery list). If left empty, all tasks will be imported.',
13 | 'things_collated allows you to switch between a single entry for all separate days (default) or separate entries for each'],
14 | 'things_tags' => '#tasks',
15 | 'things_save_hashtags' => true,
16 | 'things_project_filter' => '',
17 | 'things_collated' => true
18 | }
19 |
20 | $slog.register_plugin({ 'class' => 'ThingsLogger', 'config' => config })
21 |
22 | class ThingsLogger < Slogger
23 | def do_log
24 | if @config.key?(self.class.name)
25 | config = @config[self.class.name]
26 | filter = config['things_project_filter'] || []
27 | else
28 | @log.warn(" has not been configured or a feed is invalid, please edit your slogger_config file.")
29 | return
30 | end
31 | @log.info("Logging Things for completed tasks")
32 |
33 | # Unassigned Var
34 | #additional_config_option = config['additional_config_option'] || false
35 | config['things_tags'] ||= ''
36 | tags = config['things_tags'] == '' ? '' : "\n\n#{config['things_tags']}\n"
37 |
38 | timespan = @timespan.strftime('%d/%m/%Y')
39 | output = ''
40 | separate_days = Hash.new
41 | # Run an embedded applescript to get today's completed tasks
42 |
43 | # if filters.empty? then
44 | # filters = ["NONE", ]
45 | # end
46 |
47 | #for filter in filters
48 | values = %x{osascript <<'APPLESCRIPT'
49 | set filter to "#{filter}"
50 |
51 | setDate("#{timespan}")
52 |
53 | set dteToday to date "#{timespan}"
54 |
55 |
56 | set completedItems to ""
57 | tell application id "com.culturedcode.Things"
58 |
59 | -- Move all completed items to Logbook
60 | log completed now
61 | repeat with td in to dos of list "Logbook"
62 | set tcd to the completion date of td
63 | set dc to my intlDateFormat(tcd)
64 | repeat 1 times
65 | if (project of td) is not missing value then
66 | set aProject to project of td
67 | set projectName to name of aProject
68 |
69 | if projectName = filter then
70 | exit repeat
71 | end if
72 | end if
73 |
74 | if tcd >= dteToday then
75 | set myName to name of td
76 | set completedItems to completedItems & dc & "-::-" & myName & linefeed
77 | end if
78 | end repeat
79 | end repeat
80 | end tell
81 | return completedItems
82 |
83 | on intlDateFormat(dt)
84 | set {year:y, month:m, day:d} to dt
85 | tell (y * 10000 + m * 100 + d) as string to text 1 thru 4 & "-" & text 5 thru 6 & "-" & text 7 thru 8
86 | end intlDateFormat
87 |
88 | on setDate(theDateStr)
89 | set {TID, text item delimiters} to {text item delimiters, "/"}
90 | set {dd, mm, yy, text item delimiters} to every text item in theDateStr & TID
91 | set t to current date
92 | set day of t to (dd as integer)
93 | set month of t to (mm as integer)
94 | set year of t to (yy as integer)
95 | return t
96 | end setDate
97 |
98 | APPLESCRIPT}
99 |
100 | unless values.strip.empty?
101 | # Create entries here
102 | values.squeeze("\n").each_line do |value|
103 | # -::- is used as a delimiter as it's unlikely to show up in a todo
104 | entry = value.split('-::-')
105 | # We set the date of the entries to 23:55 and format it correctly
106 | date_to_format = entry[0] + 'T23:55:00'
107 | todo_date = Time.strptime(date_to_format, '%Y-%m-%dT%H:%M:%S')
108 | formatted_date = todo_date.utc.iso8601
109 |
110 | # create an array for the uncollated entries
111 | todo_value = separate_days.fetch(formatted_date) { '' }
112 | todo_value += "* " + entry[1]
113 | separate_days[formatted_date] = todo_value
114 |
115 | # output is used to store for collated entries
116 | output += "* " + entry[1]
117 | end
118 | end
119 | #end
120 |
121 | # Create a collated journal entry
122 | if config['things_collated'] == true
123 | unless output == ''
124 | options = {}
125 | options['content'] = "## Things - Completed Tasks\n\n#{output}\n#{tags}"
126 | sl = DayOne.new
127 | sl.to_dayone(options)
128 | end
129 | else
130 | unless separate_days.empty?
131 | # Use reduce instead of each to prevent entries from polluting the config file
132 | separate_days.reduce('') do | s, (entry_date, entry)|
133 | options = {}
134 | options['datestamp'] = entry_date
135 | options['content'] = "## Things - Completed Tasks\n\n#{entry}\n#{tags}"
136 | sl = DayOne.new
137 | sl.to_dayone(options)
138 | end
139 | end
140 | end
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/plugins_disabled/todoist.rb:
--------------------------------------------------------------------------------
1 | # Plugin: Todoist
2 | # Description: Logs completed todos from Todoist
3 | # Notes: Thanks go to Brian Stearns who inspired me to create this given his
4 | # `Things.rb` plugin.
5 | # Author: [Freddie Lindsey](twitter.com/freddielindsey)
6 |
7 |
8 | # You can add todoist_item_limit to the config (between 1 -> 50) although
9 | # I wouldn't recommend it unless you have good reason.
10 | # Ensure your todoist_token is copied below. You can find it from
11 | # the app's settings.
12 | # Note: There is no need to include hashes in the todoist_tags value ->
13 | # dayone.rb will read them anyway
14 | config = {
15 | todoist_description: [
16 | 'Logs completed todos from Todoist'
17 | ],
18 | todoist_token: '',
19 | todoist_tags: [
20 | 'todos'
21 | ]
22 | }
23 |
24 | $slog.register_plugin({ 'class' => 'TodoistLogger', 'config' => config })
25 |
26 | class TodoistLogger < Slogger
27 | def do_log
28 | if @config.key?(self.class.name)
29 | config = @config[self.class.name]
30 | unless config.key?(:todoist_token)
31 | @log.warn(
32 | "\tNo API token for todoist is present in your slogger_config\n" \
33 | "\t\t\t\t\tPlease edit your configuration file")
34 | return
35 | end
36 | else
37 | @log.warn(' has not been configured or a feed is invalid, please edit your slogger_config file.')
38 | return
39 | end
40 | @log.info("Logging Todoist for completed tasks")
41 |
42 | timespan = @timespan.strftime('%d/%m/%Y')
43 | output = ''
44 |
45 | if !config[:todoist_item_limit] ||
46 | config[:todoist_item_limit] > 50 ||
47 | config[:todoist_item_limit] < 1
48 | config[:todoist_item_limit] = 50
49 | end
50 |
51 | valid, items, projects = get_todoist_items(config)
52 | return valid if !valid
53 |
54 | entries_by_day = split_by_day(items)
55 | entries = []
56 |
57 | entries_by_day.each do |day, items|
58 | entries.push(compile_entry(day, items, projects))
59 | end
60 |
61 | count = 0
62 | entries.each do |e|
63 | count += 1
64 | options = {}
65 | options['title'] = "Todos completed on #{e[:day]}"
66 | options['content'] = e[:content]
67 | options['tags'] = config[:todoist_tags]
68 | options['datestamp'] = e[:datestamp].utc.iso8601 if e[:datestamp]
69 | sl = DayOne.new
70 | sl.to_dayone(options)
71 | end
72 |
73 | @log.info("Todoist logged #{count} #{count > 1 ? 'entries' : 'entry'}")
74 | end
75 |
76 | def get_todoist_items(config)
77 | offset = 0
78 | items = []
79 | projects = {}
80 | time_ = Time.new(@timespan.year, @timespan.month, @timespan.day)
81 | since = time_.strftime('%Y-%m-%dT%H:%M')
82 |
83 | while true
84 | begin
85 | url = URI('https://todoist.com/API/v6/get_all_completed_items')
86 | params = {
87 | token: config[:todoist_token],
88 | limit: config[:todoist_item_limit],
89 | since: since,
90 | offset: offset
91 | }
92 | url.query = URI.encode_www_form(params)
93 |
94 | res = Net::HTTP.get_response(url)
95 | rescue Exception => e
96 | @log.error("ERROR retrieving Todoist information: #{url}")
97 | return false, nil, nil
98 | end
99 |
100 | return false unless res.is_a?(Net::HTTPSuccess)
101 | json = JSON.parse(res.body)
102 |
103 | break if json['items'].length == 0
104 | break if items.select{ |item|
105 | item['task_id'] == json['items'][0]['task_id']
106 | }.length > 0
107 |
108 | items += json['items']
109 | json['projects'].each do |k, v|
110 | if projects[k]
111 | unless projects[k] == v
112 | @log.error("ERROR concurrent modification of Todoist information")
113 | return false, nil, nil
114 | end
115 | else
116 | projects[k] = v
117 | end
118 | end
119 | offset += config[:todoist_item_limit]
120 | end
121 |
122 | @log.info("Retrieved #{items.length} items in #{(offset / config[:todoist_item_limit]) + 1} requests")
123 |
124 | return true, items, projects
125 | end
126 |
127 | def get_project(projects, id)
128 | id = id.to_i
129 | projects.each do |k, v|
130 | return v if k.to_i == id
131 | end
132 | end
133 |
134 | def split_by_day(items)
135 | split = {}
136 |
137 | for i in items
138 | date = DateTime.parse(i["completed_date"])
139 | date = Time.new(date.year, date.month, date.day)
140 | split[date] = [] unless split[date]
141 | split[date].push(i)
142 | end
143 |
144 | return split
145 | end
146 |
147 | def compile_entry(day, completed_items, projects)
148 | items = {}
149 | datestamp = day
150 | completed_items.each do |item|
151 | project = get_project(projects, item["project_id"])["name"]
152 | items[project] = [] unless items[project]
153 | items[project].push(item)
154 | end
155 |
156 | entry = "# Todoist Log\n\n"
157 | entry += "### Completed Items:\n\n"
158 |
159 | items.each do |project, items|
160 | entry += "\n#### #{project}\n"
161 | items.each do |item|
162 | entry += "- #{item['content']}\n"
163 | end
164 | end
165 |
166 | entry = {
167 | content: entry,
168 | datestamp: datestamp,
169 | day: datestamp.strftime("%F")
170 | }
171 |
172 | return entry
173 | end
174 | end
175 |
--------------------------------------------------------------------------------
/plugins_disabled/traktlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: TraktLogger
3 | Description: Pull in watched items from Trakt.tv
4 | Author: [Steve Crooks](http://steve.crooks.net)
5 | Configuration:
6 | trakt_feed: "feed URL"
7 | trakt_save_image: true
8 | trakt_tv_tags: "#trakt #tv"
9 | trakt_movie_tags: "#trakt #movie"
10 | Notes:
11 | This plugin depends on a VIP subscription to trakt.tv, which enable you to get an RSS
12 | feed of movies and TV shows that you've watched.
13 | =end
14 |
15 | require 'rexml/document';
16 |
17 | config = {# description and a primary key (username, url, etc.) required
18 | 'description' => ['trakt_feed is a string containing the RSS feed for your read books',
19 | 'trakt_save_image will save the media image as the main image for the entry',
20 | 'trakt_tv_tags are tags you want to add to every TV entry',
21 | 'trakt_movie_tags are tags you want to add to every movie entry'],
22 | 'trakt_feed' => '',
23 | 'trakt_save_image' => true,
24 | 'trakt_tv_tags' => '#trakt #tv',
25 | 'trakt_movie_tags' => '#trakt #movie'
26 | }
27 |
28 | $slog.register_plugin({'class' => 'TraktLogger', 'config' => config})
29 |
30 | class TraktLogger < Slogger
31 | # @config is available with all of the keys defined in "config" above
32 | # @timespan and @dayonepath are also available
33 | # returns: nothing
34 | def do_log
35 | feed = ''
36 | if @config.key?(self.class.name)
37 | config = @config[self.class.name]
38 | # check for a required key to determine whether setup has been completed or not
39 | if !config.key?('trakt_feed') || config['trakt_feed'] == ''
40 | @log.warn("TraktLogger has not been configured or an option is invalid, please edit your slogger_config file.")
41 | return
42 | else
43 | feed = config['trakt_feed']
44 | end
45 | else
46 | @log.warn("TraktLogger has not been configured or a feed is invalid, please edit your slogger_config file.")
47 | return
48 | end
49 | @log.info("Logging TraktLogger watched media")
50 |
51 | retries = 0
52 | success = false
53 | until success
54 | if parse_feed(feed, config)
55 | success = true
56 | else
57 | break if $options[:max_retries] == retries
58 | retries += 1
59 | @log.error("Error parsing Trakt feed, retrying (#{retries}/#{$options[:max_retries]})")
60 | sleep 2
61 | end
62 | unless success
63 | @log.fatal("Could not parse feed #{feed}")
64 | end
65 | end
66 | end
67 |
68 | def parse_feed(rss_feed, config)
69 | save_image = config['trakt_save_image']
70 | unless save_image.is_a? FalseClass
71 | save_image = true
72 | end
73 |
74 | tv_tags = config['trakt_tv_tags'] || ''
75 | tv_tags = "\n\n#{tv_tags}\n" unless tv_tags == ''
76 |
77 | movie_tags = config['trakt_movie_tags'] || ''
78 | movie_tags = "\n\n#{movie_tags}\n" unless movie_tags == ''
79 |
80 | begin
81 | rss_content = ""
82 |
83 | feed_download_response = Net::HTTP.get_response(URI.parse(rss_feed))
84 | xml_data = feed_download_response.body
85 | xml_data.gsub!('media:', '') #Fix REXML unhappiness
86 | doc = REXML::Document.new(xml_data)
87 |
88 | doc.root.each_element('//entry') { |item|
89 | content = ''
90 |
91 | item_date = Time.parse(item.elements['published'].text)
92 |
93 | if item_date > @timespan
94 | title = item.elements['title'].text
95 |
96 | # is this tv or movie?
97 | is_tv = title.match(/ \d+x\d+ /) ? true : false
98 | tags = is_tv ? tv_tags : movie_tags
99 |
100 | imageurl = save_image ? item.elements['thumbnail'].attributes.get_attribute("url").value : false
101 |
102 | description = item.elements['summary'].text rescue ''
103 | description.sub!(/^.*
/, "")
104 |
105 | content += "\n\n#{description}" rescue ''
106 | options = {}
107 | header = "## Watched A #{is_tv ? 'TV Show' : 'Movie'}\n"
108 | title = title.gsub(/\n+/, ' ').strip
109 | link = item.elements['link'].attributes.get_attribute("href").value
110 | options['content'] = "#{header}[#{title}](#{link})#{content}#{tags}"
111 |
112 | options['datestamp'] = item_date.utc.iso8601
113 | options['uuid'] = %x{uuidgen}.gsub(/-/, '').strip
114 | sl = DayOne.new
115 | if imageurl
116 | sl.to_dayone(options) if sl.save_image(imageurl, options['uuid'])
117 | else
118 | sl.to_dayone(options)
119 | end
120 | else
121 | break
122 | end
123 | }
124 | rescue Exception => e
125 | @log.error("BOOM: #{e}")
126 | p e
127 | return false
128 | end
129 |
130 | true
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/plugins_disabled/wunderlistlogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: Wunderlist Logger
3 | Version: 0.1
4 | Description: Logs today's new and optionally completed/overdue tasks
5 | Notes:
6 | wl_email is your Wunderlist email address
7 | wl_password is your Wunderlist password
8 | Author: [Joe Constant](http://joeconstant.com)
9 | Configuration:
10 | wl_email:
11 | wl_password:
12 | wl_tags: "#tasks #wunderlist"
13 | wl_completed: true
14 | wl_overdue: false
15 | Notes:
16 | Requires the following gems/versions:
17 | gem 'fog-wunderlist'
18 | gem 'jwt', '~> 0.1.4'
19 | gem 'fog', '~> 1.10.0'
20 |
21 | =end
22 | config = {
23 | 'wl_description' => [
24 | 'Logs today\'s new and optionally completed/overdue tasks',
25 | 'wl_email is your Wunderlist email address',
26 | 'wl_password is your Wunderlist password'],
27 | 'wl_email' => '',
28 | 'wl_password' => '',
29 | 'wl_tags' => '#tasks #wunderlist',
30 | 'wl_completed' => true,
31 | 'wl_overdue' => false
32 | }
33 | $slog.register_plugin({ 'class' => 'WunderlistLogger', 'config' => config })
34 |
35 | require 'fog/wunderlist'
36 | require 'pp'
37 |
38 | class WunderlistLogger < Slogger
39 | def do_log
40 | if config.key?(self.class.name)
41 | config = @config[self.class.name]
42 | if !config.key?('wl_email') || config['wl_email'].empty?
43 | @log.warn("Wunderlist email has not been configured, please edit your slogger_config file.")
44 | return
45 | end
46 | if !config.key?('wl_password') || config['wl_password'].empty?
47 | @log.warn("Wunderlist password has not been configured, please edit your slogger_config file.")
48 | return
49 | end
50 | else
51 | @log.warn("Wunderlist email has not been configured, please edit your slogger_config file.")
52 | return
53 | end
54 |
55 | sl = DayOne.new
56 | config['wl_tags'] ||= ''
57 | tags = "\n\n#{config['wl_tags']}\n" unless config['wl_tags'] == ''
58 | today = @timespan.to_i
59 |
60 | @log.info("Getting Wunderlist tasks for #{config['wl_email']}")
61 | if config['wl_completed']
62 | @log.info("completed: true")
63 | end
64 | if config['wl_overdue']
65 | @log.info("overdue: true")
66 | end
67 | output = ''
68 |
69 | begin
70 | service = Fog::Tasks.new :provider => 'Wunderlist',
71 | :wunderlist_username => config['wl_email'],
72 | :wunderlist_password => config['wl_password']
73 |
74 | newoutput = ''
75 | completeoutput = ''
76 | overdueoutput = ''
77 | service.tasks.each do |task|
78 | if task.created_at.to_i > today
79 | newoutput += "* #{task.title}\n"
80 | end
81 | if config['wl_completed']
82 | if task.completed_at.to_i > today
83 | completeoutput += "* #{task.title}\n"
84 | end
85 | end
86 | if config['wl_overdue']
87 | if task.completed_at.nil? && !task.due_date.nil? && task.due_date.to_i < today
88 | list = service.lists.find { |l| l.id == task.list_id }
89 | overdueoutput += "* #{task.title} on list '#{list.title}' was due '#{task.due_date}'\n"
90 | end
91 | end
92 | end
93 | unless newoutput == ''
94 | output += "## New\n#{newoutput}\n\n"
95 | end
96 | unless completeoutput == ''
97 | output += "## Completed\n#{completeoutput}\n\n"
98 | end
99 | unless overdueoutput == ''
100 | output += "## Overdue\n#{overdueoutput}\n\n"
101 | end
102 |
103 | unless output == ''
104 | options = {}
105 | options['content'] = "# Wunderlist tasks\n\n#{output}#{tags}"
106 | sl.to_dayone(options)
107 | end
108 |
109 | rescue Exception => e
110 | puts "Error getting tasks"
111 | p e
112 | return ''
113 | end
114 | end
115 | end
--------------------------------------------------------------------------------
/plugins_disabled/yahoofinancelogger.rb:
--------------------------------------------------------------------------------
1 | =begin
2 | Plugin: YahooFinanceLogger
3 | Description: Logs a portfolio of prices from Yahoo finance
4 | Author: [Hilton Lipschitz](http://www.hiltmon.com)
5 | Configuration:
6 | - tickers: an array of valid Yahoo tickers to log
7 | - show_details: If true, adds day and 52 week high and low, volume, P/E and Market Cap
8 | Notes:
9 | - Does not run on weekends as the markets are closed (but does run on holidays)
10 | - Runs in real time, so if run during the day, will get as at the run time values
11 | =end
12 |
13 | config = {
14 | 'description' => [
15 | 'Logs up to yesterday\`s Yahoo Finance prices',
16 | 'tickers are a list of the Yahoo Finance Tickers you want (^GSPC => S&P500, ^IXIC => NasDaq, ^DJI => DowJones, AAPL => Apple Inc, AUDUSD=X => AUD/USD, USDJPY=X => USD/JPY, ^TNX => 10-Year Bond)',
17 | 'show_details adds day and 52 week high and low, volume, P/E and Market Cap'
18 | ],
19 | 'tickers' => [ '^GSPC', '^IXIC', '^DJI', 'AAPL', 'GOOG' ], # A list of Yahoo Finance Tickers to log
20 | 'show_details' => true,
21 | 'tags' => '#social #finance'
22 | }
23 |
24 | $slog.register_plugin({ 'class' => 'YahooFinanceLogger', 'config' => config })
25 |
26 | require 'CSV'
27 |
28 | class YahooFinanceLogger < Slogger
29 |
30 | def do_log
31 | if @config.key?(self.class.name)
32 | config = @config[self.class.name]
33 | # check for a required key to determine whether setup has been completed or not
34 | if !config.key?('tickers') || config['tickers'] == []
35 | @log.warn(" has not been configured or an option is invalid, please edit your slogger_config file.")
36 | return
37 | end
38 | else
39 | @log.warn(" has not been configured or a feed is invalid, please edit your slogger_config file.")
40 | return
41 | end
42 | @log.info("Logging Tickers")
43 |
44 | tickers = config['tickers']
45 | @tags = config['tags'] || ''
46 | tags = "\n\n#{@tags}\n" unless @tags == ''
47 | show_details = (config['show_details'] == true)
48 |
49 | # This logger gets real-time data from Yahoo, so whatever time you run it, that's the data
50 | # I prefer to run my Slogger late at night, so this gets me the day's close
51 | weekday_now = Time.now.strftime('%a')
52 | if weekday_now == 'Sat' || weekday_now == 'Sun'
53 | @log.warn("Its a weekend, nothing to do.")
54 | return
55 | end
56 |
57 | symbols = tickers.join("+")
58 | symbols = tickers.join("+")
59 | uri = URI(URI.escape("http://download.finance.yahoo.com/d/quotes.csv?s=#{symbols}&f=nl1c1oghjkpvrj1"))
60 |
61 | res = Net::HTTP.get_response(uri)
62 | unless res.is_a?(Net::HTTPSuccess)
63 | @log.warn("Unable to get data from Yahoo Finance.")
64 | return
65 | end
66 |
67 | data = CSV.parse(res.body)
68 |
69 | content = []
70 | data.each do |row|
71 | if show_details == true
72 | content << "* **#{row[0]}**: #{commas(row[1])} (#{row[2]}%)\n Low: #{commas(row[4])} (52 Low: #{commas(row[6])})\n High: #{commas(row[5])} (52 High: #{commas(row[7])})\n Volume: #{commas(row[9])}\n P/E Ratio: #{commas(row[10])}\n Market Cap: #{commas(row[11])}"
73 | else
74 | content << "* **#{row[0]}**: #{commas(row[1])} (#{row[2]}%)"
75 | end
76 | end
77 |
78 | # And log it
79 | options = {}
80 | options['content'] = "## Today\'s Markets\n\n#{content.join("\n\n")}\n\n#{tags}"
81 | options['datestamp'] = Time.now.utc.iso8601
82 | # options['starred'] = true
83 | # options['uuid'] = %x{uuidgen}.gsub(/-/,'').strip
84 |
85 | sl = DayOne.new
86 | sl.to_dayone(options)
87 | end
88 |
89 | def commas(value)
90 | value.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/rssfeedlist.md:
--------------------------------------------------------------------------------
1 | #RSS Feed Resource for Brett Terpstra's Slogger
2 |
3 | I hacked this together from numerous places as a resource for those using [Slogger](https://github.com/ttscoff/Slogger) (graciously offered by Brett Terpstra).
4 |
5 | I have tried to use ALL_CAPS in the feeds to note those areas that will require your specific info.
6 |
7 | Feel free to share the list and make additions. And please let me know if there is anything here that needs correcting.
8 |
9 | ##App.net
10 | * Feed
11 | * https://alpha-api.app.net/feed/rss/users/USERNAME/posts
12 | * Hashtag
13 | * https://alpha-api.app.net/feed/rss/posts/tag/HASHTAG
14 |
15 | ##Blogger
16 | * Feed
17 | * http://BLOGNAME.blogspot.com/rss.xml
18 |
19 | ##Dropbox
20 | * Feed
21 | * https://www.dropbox.com/123456/7891011/a12b345/events.xml
22 |
23 | Note: – Via the Dropbox web interface, enable RSS feeds under your Dropbox Settings. While still in the webinterface, go to your Events page or use the URL http://dropbox.com/events. Scroll to bottom of page and look for"Subscribe to this feed." link and click on it to get the feed for all your Dropbox events.
24 |
25 | ##Dropmark
26 | * Feed
27 | * http://demo.dropmark.com/110 becomes http://demo.dropmark.com/110.rss
28 |
29 | Note: Add ".rss" to your collection URL to get a direct link to its RSS feed.
30 |
31 | ##Evernote
32 | * Feed
33 | * https://www.evernote.com/pub/USERNAME/NOTEBOOK/feed
34 |
35 | Note: you can also get the RSS feed for any shared notebook on Evernote.
36 |
37 | ##Facebook
38 | * Feed - Individual Profile
39 | * No RSS feeds for individual profiles.
40 | * Feed - Facebook Pages
41 | * https://www.facebook.com/feeds/page.php?format=atom10&id=FACEBOOK_ID
42 |
43 | Note: If you don't have a custom page URL, your FACEBOOK_ID will show up when you access the page. If you do have a custompage URL, go to the FB page, scroll down to the 'like this' link, right click and copy link. Then paste it in yourtext editor or somewhere else to view your ID.
44 |
45 | ##Flickr
46 | * Feed - User
47 | * http://api.flickr.com/services/feeds/photos_public.gne?id=FLICKR_ID
48 |
49 | Note: use http://idgettr.com to get your FLICKR_ID.
50 | * Feed - Tags (separate tags with commas)
51 | * http://api.flickr.com/services/feeds/photos_public.gne?tags=,
52 |
53 | ##Foursquare
54 | * Feed
55 | * https://feeds.foursquare.com/history/ABCD.rss
56 |
57 | Note: Via the Foursquare web interface, enter URL http://foursquare.com/feeds/ after signing in.
58 |
59 | ##Instagram
60 | * Feed - Tags
61 | * http://instagr.am/tags/TAG/feed/recent.rss
62 |
63 | Note: There is not an official Instagram feed for individual users, but there are third party services that can do so Perform a Google search for options.
64 | One option is Webstagram - http://web.stagram.com
65 | Create account that will access your Instagram account.
66 | Feed will be in the form of http://widget.stagram.com/rss/n/INSTAGRAM_ID/
67 |
68 | ##InstaPaper
69 | * Feed
70 | * http://www.instapaper.com/rss/123/456
71 |
72 | Note: – Via the Instapaper web interface, scroll to the bottom of the page for "This folder's RSS" link.
73 |
74 | ##LinkedIn
75 | * Feed
76 | * http://www.linkedin.com/rss/nus?key=ABCDEF
77 |
78 | Note: Via the LinkedIn web interface, enter URL http://www.linkedin.com/rssAdmin?display= after signing in.
79 |
80 | ##Picasa
81 |
82 | * Feed - Search query
83 | * http://photos.googleapis.com/data/feed/base/all?alt=rss&kind=photo&q=SEARCH_TERM
84 |
85 | ##Pinterest
86 | * Feed - User
87 | * http://pinterest.com/USERNAME/feed.rss
88 | * Feed - Board
89 | * http://pinterest.com/USERNAME/BOARD_NAME/rss
90 |
91 | ##StumbleUpon
92 | * Feed
93 | * http://rss.stumbleupon.com/user/USERNAME/favorites
94 |
95 | ##Twitter
96 | * Feed - User Timeline
97 | * https://twitter.com/statuses/user_timeline/USERNAME.rss
98 | * Feed - Favorite Tweets
99 | * https://api.twitter.com/1/favorites/USERNAME.rss
100 | * Feed - @Mentions
101 | * http://search.twitter.com/search.rss?q=to:@USERNAME
102 | * Feed - Hashtag or Search query
103 | * http://search.twitter.com/search.rss?q=QUERY
104 | * Feed - Twitter List
105 | * https://api.twitter.com/1/USERNAME/lists/LISTNAME/statuses.atom
106 |
107 | Note: if LISTNAME contains two or more words with spaces between them, use %20 as the separator in place of the space.
108 |
109 | ##Tumblr
110 | * Feed
111 | * http://BLOG_NAME.tumblr.com/rss
112 | * Feed - Tag
113 | * http://BLOG_NAME.tumblr.com/tagged/TAG_NAME/rss
114 |
115 | ##WordPress hosted
116 | * Feed
117 | * http://BLOG_NAME.wordpress.com/feed/
118 | * Feed - Tag
119 | * http://BLOG_NAME.wordpress.com/tag/TAG_NAME/feed/
120 |
121 | ##YouTube
122 | * Feed - Recent uploads
123 | * https://gdata.youtube.com/feeds/api/users/USERNAME/uploads
124 | * Feed - Tag
125 | * https://gdata.youtube.com/feeds/api/videos/-/TAG
126 | * Feed - Search query
127 | * https://gdata.youtube.com/feeds/api/videos?q=QUERY
128 |
129 | Note: you can add the following after QUERY to refine:
130 | &orderby=relevance
131 | &orderby=published
132 | &orderby=viewCount
133 |
134 | ##For services that do not offer an RSS feed
135 |
136 | * Determine if there is a way to post the data to Twitter. If so, you're in business. You can simply post to your current Twitter account and pull in via Slogger.
137 | * Or, if you don't want to clutter up your regular Twitter account, create a new one to house these feeds and add thenewly created Twitter account to Slogger.
--------------------------------------------------------------------------------
/slogger:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # __ _
3 | # / _\| | ___ __ _ __ _ ___ _ __
4 | # \ \ | |/ _ \ / _` |/ _` |/ _ \ '__|
5 | # _\ \| | (_) | (_| | (_| | __/ |
6 | # \__/|_|\___/ \__, |\__, |\___|_|
7 | # |___/ |___/
8 | # Copyright 2012, Brett Terpstra
9 | # http://brettterpstra.com
10 | # --------------------
11 | MAJOR_VERSION = 2
12 | MINOR_VERSION = 1
13 | BUILD_NUMBER = 14
14 |
15 | init_env = ENV['SLOGGER_NO_INITIALIZE'].to_s
16 | ENV['SLOGGER_NO_INITIALIZE'] = "false"
17 |
18 | require File.expand_path('../slogger',__FILE__)
19 |
20 | ENV['SLOGGER_NO_INITIALIZE'] = init_env
21 |
--------------------------------------------------------------------------------
/slogger.develop.rb:
--------------------------------------------------------------------------------
1 | slogger
--------------------------------------------------------------------------------
/slogger_image.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | # encoding: utf-8
3 |
4 | if ARGV.nil? || ARGV.length < 1
5 | raise "Slogger Image requires that you feed it a filename."
6 | Process.exit(-1)
7 | end
8 |
9 | slogger = File.dirname(__FILE__) + '/slogger'
10 | %x{"#{slogger}" "#{ARGV[0]}"}
11 |
--------------------------------------------------------------------------------
/spec/plugins/fixtures/strava.yml:
--------------------------------------------------------------------------------
1 | ---
2 | http_interactions:
3 | - request:
4 | method: get
5 | uri: https://www.strava.com/api/v3/athlete/activities?access_token=the_access_token
6 | body:
7 | encoding: US-ASCII
8 | string: ''
9 | headers:
10 | Accept-Encoding:
11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
12 | Accept:
13 | - '*/*'
14 | User-Agent:
15 | - Ruby
16 | Host:
17 | - www.strava.com
18 | response:
19 | status:
20 | code: 200
21 | message: OK
22 | headers:
23 | Content-Type:
24 | - application/json; charset=utf-8
25 | Status:
26 | - '200'
27 | X-Ratelimit-Limit:
28 | - '600,30000'
29 | X-Ratelimit-Usage:
30 | - '1,4'
31 | X-Ua-Compatible:
32 | - IE=Edge,chrome=1
33 | Content-Length:
34 | - '1481'
35 | Connection:
36 | - keep-alive
37 | body:
38 | encoding: UTF-8
39 | string: '[{"id":114100845,"resource_state":2,"external_id":null,"upload_id":null,"athlete":{"id":3880480,"resource_state":1},"name":"Afternoon
40 | Walk","distance":3218.7,"moving_time":1894,"elapsed_time":1894,"total_elevation_gain":0.0,"type":"Walk","start_date":"2014-02-17T23:59:58Z","start_date_local":"2014-02-17T17:59:58Z","timezone":"(GMT-06:00)
41 | America/Chicago","start_latlng":null,"end_latlng":null,"location_city":null,"location_state":null,"location_country":"United
42 | States","start_latitude":null,"start_longitude":null,"achievement_count":0,"kudos_count":0,"comment_count":0,"athlete_count":1,"photo_count":0,"map":{"id":"a114100845","summary_polyline":null,"resource_state":2},"trainer":false,"commute":false,"manual":true,"private":false,"flagged":false,"gear_id":null,"average_speed":1.7,"max_speed":0.0,"calories":0,"truncated":null,"has_kudoed":false},{"id":113800428,"resource_state":2,"external_id":"3E31A010-63C0-45C2-B5F3-1102EBEE44CF","upload_id":124431086,"athlete":{"id":3880480,"resource_state":1},"name":"Afternoon
43 | Walk","distance":3161.5,"moving_time":2018,"elapsed_time":2357,"total_elevation_gain":0.0,"type":"Walk","start_date":"2014-02-16T19:02:12Z","start_date_local":"2014-02-16T13:02:12Z","timezone":"(GMT-06:00)
44 | America/Chicago","start_latlng":[41.85,-94.02],"end_latlng":[41.85,-94.02],"location_city":"Bouton","location_state":"IA","location_country":"United
45 | States","start_latitude":41.85,"start_longitude":-94.02,"achievement_count":0,"kudos_count":0,"comment_count":0,"athlete_count":1,"photo_count":0,"map":{"id":"a113800428","summary_polyline":"_km~Fjlz|PbBsl@bLoAQ{EqK??w[rDf@f@bGtDf@P~WoKnAkCnn@","resource_state":2},"trainer":false,"commute":false,"manual":false,"private":false,"flagged":false,"gear_id":null,"average_speed":1.6,"max_speed":4.1,"calories":0,"truncated":null,"has_kudoed":false}]'
46 | http_version:
47 | recorded_at: Tue, 18 Feb 2014 13:01:34 GMT
48 | recorded_with: VCR 2.8.0
49 |
50 | #[
51 | # {
52 | # "id": 114100845,
53 | # "resource_state": 2,
54 | # "external_id": null,
55 | # "upload_id": null,
56 | # "athlete": {
57 | # "id": 3880480,
58 | # "resource_state": 1
59 | # },
60 | # "name": "Afternoon Walk",
61 | # "distance": 3218.7,
62 | # "moving_time": 1894,
63 | # "elapsed_time": 1894,
64 | # "total_elevation_gain": 0.0,
65 | # "type": "Walk",
66 | # "start_date": "2014-02-17T23:59:58Z",
67 | # "start_date_local": "2014-02-17T17:59:58Z",
68 | # "timezone": "(GMT-06:00) America/Chicago",
69 | # "start_latlng": null,
70 | # "end_latlng": null,
71 | # "location_city": null,
72 | # "location_state": null,
73 | # "location_country": "United States",
74 | # "start_latitude": null,
75 | # "start_longitude": null,
76 | # "achievement_count": 0,
77 | # "kudos_count": 0,
78 | # "comment_count": 0,
79 | # "athlete_count": 1,
80 | # "photo_count": 0,
81 | # "map": {
82 | # "id": "a114100845",
83 | # "summary_polyline": null,
84 | # "resource_state": 2
85 | # },
86 | # "trainer": false,
87 | # "commute": false,
88 | # "manual": true,
89 | # "private": false,
90 | # "flagged": false,
91 | # "gear_id": null,
92 | # "average_speed": 1.7,
93 | # "max_speed": 0.0,
94 | # "calories": 0,
95 | # "truncated": null,
96 | # "has_kudoed": false
97 | # },
98 | # {
99 | # "id": 113800428,
100 | # "resource_state": 2,
101 | # "external_id": "3E31A010-63C0-45C2-B5F3-1102EBEE44CF",
102 | # "upload_id": 124431086,
103 | # "athlete": {
104 | # "id": 3880480,
105 | # "resource_state": 1
106 | # },
107 | # "name": "Afternoon Walk",
108 | # "distance": 3161.5,
109 | # "moving_time": 2018,
110 | # "elapsed_time": 2357,
111 | # "total_elevation_gain": 0.0,
112 | # "type": "Walk",
113 | # "start_date": "2014-02-16T19:02:12Z",
114 | # "start_date_local": "2014-02-16T13:02:12Z",
115 | # "timezone": "(GMT-06:00) America/Chicago",
116 | # "start_latlng": [
117 | # 41.85,
118 | # -94.02
119 | # ],
120 | # "end_latlng": [
121 | # 41.85,
122 | # -94.02
123 | # ],
124 | # "location_city": "Bouton",
125 | # "location_state": "IA",
126 | # "location_country": "United States",
127 | # "start_latitude": 41.85,
128 | # "start_longitude": -94.02,
129 | # "achievement_count": 0,
130 | # "kudos_count": 0,
131 | # "comment_count": 0,
132 | # "athlete_count": 1,
133 | # "photo_count": 0,
134 | # "map": {
135 | # "id": "a113800428",
136 | # "summary_polyline": "_km~Fjlz|PbBsl@bLoAQ{EqK??w[rDf@f@bGtDf@P~WoKnAkCnn@",
137 | # "resource_state": 2
138 | # },
139 | # "trainer": false,
140 | # "commute": false,
141 | # "manual": false,
142 | # "private": false,
143 | # "flagged": false,
144 | # "gear_id": null,
145 | # "average_speed": 1.6,
146 | # "max_speed": 4.1,
147 | # "calories": 0,
148 | # "truncated": null,
149 | # "has_kudoed": false
150 | # }
151 | #]
152 |
--------------------------------------------------------------------------------
/spec/plugins/mock_day_one.rb:
--------------------------------------------------------------------------------
1 | class DayOne
2 | class << self
3 | attr_accessor :to_dayone_options
4 | end
5 |
6 | def to_dayone(options)
7 | DayOne.to_dayone_options ||= []
8 | DayOne.to_dayone_options << options
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/plugins/mock_slogger.rb:
--------------------------------------------------------------------------------
1 | class Slogger
2 | RSpec::Mocks::setup(self)
3 | $slog = double.as_null_object
4 |
5 | attr_accessor :config, :log, :timespan
6 |
7 | def initialize
8 | RSpec::Mocks::setup(self)
9 | @config = {}
10 | @log = double.as_null_object
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/plugins/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $:.unshift File.join(File.dirname(__FILE__))
2 | $:.unshift File.join(File.dirname(__FILE__), '..', '..', 'plugins')
3 | $:.unshift File.join(File.dirname(__FILE__), '..', '..', 'plugins_disabled')
4 |
5 | require 'mock_slogger'
6 | require 'mock_day_one'
7 | require 'vcr'
8 |
9 | class String
10 | def unindent
11 | gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, "")
12 | end
13 | end
14 |
15 | VCR.configure do |c|
16 | c.cassette_library_dir = File.join(File.dirname(__FILE__), 'fixtures')
17 | c.hook_into :webmock
18 | end
19 |
20 | RSpec.configure do |config|
21 | config.color_enabled = true
22 | end
23 |
24 |
--------------------------------------------------------------------------------
/spec/plugins/stravalogger_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'spec_helper'
2 | require 'stravalogger'
3 |
4 | describe StravaLogger do
5 | let(:strava) {
6 | StravaLogger.new.tap do |strava|
7 | strava.config = {
8 | 'StravaLogger' => {
9 | 'strava_access_token' => 'the_access_token',
10 | 'strava_unit' => 'imperial',
11 | 'strava_tags' => '#the_tags'
12 | }
13 | }
14 | end
15 | }
16 |
17 | it 'warns if config not found' do
18 | strava.config.delete('StravaLogger')
19 | strava.log.should_receive(:warn).with('Strava has not been configured or is invalid, please edit your slogger_config file.')
20 | strava.do_log
21 | end
22 |
23 | it 'warns if access_token is not set' do
24 | strava.config['StravaLogger']['strava_access_token'] = ' '
25 | strava.log.should_receive(:warn).with('Strava access token has not been configured, please edit your slogger_config file.')
26 | strava.do_log
27 | end
28 |
29 | it 'does not log anything if there are no activities newer than the timespan' do
30 | VCR.use_cassette('strava') do
31 | strava.timespan = Time.now
32 | strava.do_log
33 | end
34 | end
35 |
36 | it 'logs the activity to DayOne' do
37 | VCR.use_cassette('strava') do
38 | strava.timespan = Time.parse('2014-02-17 00:00:00')
39 | strava.do_log
40 |
41 | DayOne.to_dayone_options.size.should == 1
42 | options = DayOne.to_dayone_options.first
43 | options['uuid'].should_not be_nil
44 | options['starred'].should be_false
45 | options['datestamp'].should == '2014-02-17T23:59:58Z'
46 |
47 | expected_content = <<-eos.unindent
48 | # Strava Activity - 2.00 mi - 0h 31m 34s - 3.8 mph - Afternoon Walk
49 |
50 | * **Type**: Walk
51 | * **Distance**: 2.00 mi
52 | * **Elevation Gain**: 0 ft
53 | * **Average Speed**: 3.8 mph
54 | * **Max Speed**: 0.0 mph
55 | * **Elapsed Time**: 00:31:34
56 | * **Moving Time**: 00:31:34
57 | * **Link**: http://www.strava.com/activities/114100845
58 |
59 |
60 | #the_tags
61 | eos
62 |
63 | options['content'].should == expected_content
64 | end
65 | end
66 | end
67 |
68 |
--------------------------------------------------------------------------------
/test:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------