├── doing-plugin-twitter-import.gemspec ├── README.md └── lib └── doing-plugin-twitter-import.rb /doing-plugin-twitter-import.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "doing-plugin-twitter-import" 3 | s.version = "0.0.3" 4 | s.summary = "Twitter timeline import for Doing" 5 | s.description = "Imports entries from the Twitter timeline to Doing" 6 | s.authors = ["Brett Terpstra"] 7 | s.email = "me@brettterpstra.com" 8 | s.files = ["lib/doing-plugin-twitter-import.rb"] 9 | s.homepage = "https://brettterpstra.com" 10 | s.license = "MIT" 11 | s.add_runtime_dependency('twitter', '~> 7.0') 12 | end 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doing plugin: Twitter Import 2 | 3 | This is a plugin for [Doing](http://brettterpstra.com/projects/doing) which imports a user's tweets as Doing entries. 4 | 5 | ## Installation 6 | 7 | Just run `gem install doing-plugin-twitter-import`. You may need to run with `sudo`, depending on your setup. 8 | 9 | ## Configuration 10 | 11 | To set up, you need to [register an app with Twitter](https://apps.twitter.com/) and add its credentials to your Doing config. To create the config keys, run `doing config refresh`, then use `doing config open` to open the main `config.yml` file. You'll see the necessary fields (api_key, api_secret, and user) under the plugins->twitter section of the config. 12 | 13 | The user key will probably be your own Twitter handle, but you could grab any user's tweets if you fancy. 14 | 15 | ## Usage 16 | 17 | Once you have credentials set up, you can run an import: 18 | 19 | doing import --type twitter --tag twitter --prefix "Tweet: " 20 | 21 | The `tag` and `prefix` options are entirely optional. 22 | 23 | The first time you run it, it will grab the last 200 tweets, including retweets but excluding replies. It will store the id of the most recent tweet, and the next time you run the import, it will only get tweets newer than that tweet. The plugin will automatically avoid duplicating tweets in your Doing file. 24 | -------------------------------------------------------------------------------- /lib/doing-plugin-twitter-import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # title: Twitter Import 4 | # description: Import entries from a Twitter timeline 5 | # author: Brett Terpstra 6 | # url: https://brettterpstra.com 7 | module Doing 8 | # Twitter import plugin 9 | class TwitterImport 10 | require 'time' 11 | require 'twitter' 12 | 13 | include Doing::Util 14 | include Doing::Errors 15 | 16 | def self.settings 17 | { 18 | trigger: '^tw(?:itter)?$', 19 | config: { 'api_key' => 'xxxxx', 'api_secret' => 'xxxxx', 'user' => 'xxxxx' } 20 | } 21 | end 22 | 23 | ## 24 | ## Imports a Twitter timeline as Doing entries 25 | ## 26 | ## @param wwid [WWID] WWID object 27 | ## @param path [String] Not used 28 | ## @param options [Hash] Additional Options 29 | ## 30 | def self.import(wwid, path, options: {}) 31 | key = Doing.setting('plugins.twitter.api_key') 32 | secret = Doing.setting('plugins.twitter.api_secret') 33 | 34 | raise PluginException, 'Please add Twitter API key/secret to config. Run `doing config refresh` to add placeholders.' if key =~ /^xxxxx/ 35 | 36 | user = Doing.setting('plugins.twitter.user') 37 | 38 | @client = Twitter::REST::Client.new do |config| 39 | config.consumer_key = key 40 | config.consumer_secret = secret 41 | end 42 | 43 | options[:no_overlap] = true 44 | options[:autotag] ||= wwid.auto_tag 45 | 46 | tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : [] 47 | options[:tag] = nil 48 | prefix = options[:prefix] || '' 49 | 50 | @old_items = wwid.content 51 | 52 | new_items = load_timeline(user, wwid, { prefix: prefix, tags: tags, section: options[:section], autotag: options[:autotag] }) 53 | 54 | return if new_items.nil? 55 | 56 | total = new_items.count 57 | 58 | options[:count] = 0 59 | 60 | new_items = wwid.filter_items(new_items, opt: options) 61 | 62 | skipped = total - new_items.count 63 | Doing.logger.debug('Skipped:' , %(#{skipped} items that didn't match filter criteria)) if skipped.positive? 64 | 65 | imported = [] 66 | 67 | new_items.each do |item| 68 | next if duplicate?(item) 69 | 70 | imported.push(item) 71 | end 72 | 73 | dups = new_items.count - imported.count 74 | Doing.logger.info('Skipped:', %(#{dups} duplicate items)) if dups.positive? 75 | 76 | imported = wwid.dedup(imported, no_overlap: !options[:overlap]) 77 | overlaps = new_items.count - imported.count - dups 78 | Doing.logger.debug('Skipped:', "#{overlaps} items with overlapping times") if overlaps.positive? 79 | 80 | imported.each do |item| 81 | wwid.content.add_section(item.section) 82 | wwid.content.push(item) 83 | end 84 | 85 | Doing.logger.info('Imported:', "#{imported.count} items") 86 | end 87 | 88 | def self.duplicate?(item) 89 | @old_items.each do |oi| 90 | return true if item.equal?(oi) 91 | end 92 | 93 | false 94 | end 95 | 96 | def self.load_timeline(user, wwid, options) 97 | config_dir = File.join(Util.user_home, '.config', 'doing') 98 | id_storage = File.join(config_dir, 'last_tweet_id') 99 | Doing.logger.log_now(:info, 'Twitter:', "retrieving timeline for #{user}") 100 | 101 | if File.exist?(id_storage) 102 | last_id = IO.read(id_storage).strip.to_i 103 | else 104 | last_id = nil 105 | end 106 | 107 | tweet_options = { 108 | count: 200, 109 | include_rts: true, 110 | exclude_replies: true 111 | } 112 | tweet_options[:since_id] = last_id if last_id 113 | 114 | tweets = @client.user_timeline(user, tweet_options).map do |t| 115 | { date: t[:created_at], title: t[:text], id: t[:id] } 116 | end 117 | if !tweets.nil? && tweets.count.positive? 118 | Doing.logger.log_now(:info, 'Twitter:', "found #{tweets.count} new tweets") 119 | else 120 | Doing.logger.log_now(:info, 'Twitter:', 'no new tweets found') 121 | return 122 | end 123 | 124 | items = [] 125 | 126 | tweets.reverse.each do |tweet| 127 | last_id = tweet[:id] if tweet[:id] 128 | date = Time.parse(tweet[:date].strftime('%Y-%m-%d %H:%M -0000')).localtime 129 | text = tweet[:title].dup 130 | text = text.force_encoding('utf-8') if text.respond_to? :force_encoding 131 | input = text.split("\n") 132 | title = input[0] 133 | note = Note.new(input.slice(1, input.count)) 134 | 135 | title = "#{options[:prefix]} #{title} @done" 136 | options[:tags].each do |tag| 137 | if title =~ /\b#{tag}\b/i 138 | title.sub!(/\b#{tag}\b/i, "@#{tag}") 139 | else 140 | title += " @#{tag}" 141 | end 142 | end 143 | title = wwid.autotag(title) if options[:autotag] 144 | title.gsub!(/ +/, ' ') 145 | title.strip! 146 | section = options[:section] || wwid.config['current_section'] 147 | 148 | new_item = Item.new(date, title, section) 149 | new_item.note = note 150 | 151 | items << new_item if new_item 152 | end 153 | 154 | FileUtils.mkdir(config_dir) unless File.exist?(config_dir) 155 | File.open(id_storage, 'w+') do |f| 156 | f.puts last_id 157 | end 158 | 159 | items 160 | end 161 | 162 | Doing::Plugins.register 'twitter', :import, self 163 | end 164 | end 165 | --------------------------------------------------------------------------------