├── .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 | 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 &#123; 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 | "![alt text](#{album['icon']})#{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/​t​a​g​g​e​d​/​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 | --------------------------------------------------------------------------------