├── .deepsource.toml ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── lib ├── config │ └── mongoid.yml ├── fana_os.rb └── fana_os │ ├── obsidian_utils.rb │ └── pocket_utils.rb └── oauth_server.rb /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "ruby" 5 | enabled = true 6 | 7 | [[transformers]] 8 | name = "rubocop" 9 | enabled = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Ruby infra 4 | gem 'dotenv' 5 | gem 'sinatra' 6 | gem 'mongoid', '~> 7.0' 7 | 8 | # APIs 9 | gem 'pocket-ruby' 10 | gem 'git' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (6.0.3.4) 5 | activesupport (= 6.0.3.4) 6 | activesupport (6.0.3.4) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 0.7, < 2) 9 | minitest (~> 5.1) 10 | tzinfo (~> 1.1) 11 | zeitwerk (~> 2.2, >= 2.2.2) 12 | bson (4.11.1) 13 | concurrent-ruby (1.1.7) 14 | dotenv (2.7.6) 15 | faraday (0.17.3) 16 | multipart-post (>= 1.2, < 3) 17 | faraday_middleware (0.14.0) 18 | faraday (>= 0.7.4, < 1.0) 19 | hashie (4.1.0) 20 | i18n (1.8.5) 21 | concurrent-ruby (~> 1.0) 22 | minitest (5.14.2) 23 | mongo (2.13.1) 24 | bson (>= 4.8.2, < 5.0.0) 25 | mongoid (7.1.5) 26 | activemodel (>= 5.1, < 6.1) 27 | mongo (>= 2.7.0, < 3.0.0) 28 | multi_json (1.15.0) 29 | multipart-post (2.1.1) 30 | mustermann (1.1.1) 31 | ruby2_keywords (~> 0.0.1) 32 | pocket-ruby (0.0.6) 33 | faraday (>= 0.7) 34 | faraday_middleware (~> 0.9) 35 | hashie (>= 0.4.0) 36 | multi_json (~> 1.0, >= 1.0.3) 37 | rack (2.2.3) 38 | rack-protection (2.1.0) 39 | rack 40 | ruby2_keywords (0.0.2) 41 | sinatra (2.1.0) 42 | mustermann (~> 1.0) 43 | rack (~> 2.2) 44 | rack-protection (= 2.1.0) 45 | tilt (~> 2.0) 46 | thread_safe (0.3.6) 47 | tilt (2.0.10) 48 | tzinfo (1.2.8) 49 | thread_safe (~> 0.1) 50 | zeitwerk (2.4.1) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | dotenv 57 | mongoid (~> 7.0) 58 | pocket-ruby 59 | sinatra 60 | 61 | BUNDLED WITH 62 | 1.17.3 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fana-os 2 | This is my operating system. High level: 3 | 4 | - Knowledge management: Obsidian 5 | - Reading (web): Pocket 6 | - Reading (pdfs): [Obsidian Annotator](https://github.com/elias-sundqvist/obsidian-annotator) 7 | - Email: Superhuman 8 | - Code editor: VSC 9 | - Daily work: Custom built software @ Decibel 10 | 11 | ### Pocket 12 | 13 | I use Pocket to store articles, read them, as well as highlighting them. In the PocketUtils class, you can find a simple exporter to Obsidian: 14 | 15 | - Fetch all Pocket articles in your account 16 | - Save them in a local Mongo instance (title, URL, tags etc) 17 | - Based on what day I read the article, it adds an entry in the corresponding Obsidian daily notes doc. Here's an example: 18 | 19 | ``` 20 | **[The Opportunity and Risks for Consumer Startups in a Social Distancing World — A Framework for…](https://medium.com/@sarahtavel/the-opportunity-and-risks-for-consumer-startups-in-a-social-distancing-world-a-framework-for-15f65e2fbdff)** 21 | *By Sarah Tavel* 22 | Tags: 23 | - Some rocks (such as TV) are easier to fit in because they are porous rocks, making multi-tasking easier (like scanning Twitter during slow parts of a show). 24 | - Water is different than rocks and sand. Water don’t require dominant attention and can permeate some rocks and all sand time.* For example, something like Discord is a perfect water for a gaming rock. 25 | - Events that span bigger blocks of contiguous time (“Rocks”), micro events that take advantage of attention gaps between or during those blocks (“Sand”), and things that can overlay over the other two (“Water”). 26 | ``` 27 | 28 | To get the access token you can use the Sinatra server built in (Start it with `ruby oauth_server.rb`). In order to get highlights, you should use the consumer key obtained from the Pocket web app. To get that, use the Chrome dev tools and observe the XHR requests; they will include the `consumer_key` as a parameter. Then plug that into your `.env` file. 29 | 30 | ### Obsidian 31 | 32 | - My main Obsidian folder is a git repo. I have a script to add all to it, timestamp the commit and send to GitHub for storage. 33 | 34 | ## In the pipeline 35 | 36 | ### Remote db support 37 | 38 | Right now I use a local Mongo to store data. Should move this to an hosted instance. 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :console do 2 | exec "RACK_ENV=development irb -r fana_os -I ./lib" 3 | end -------------------------------------------------------------------------------- /lib/config/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | clients: 3 | default: 4 | #uri: mongodb://127.0.0.1:27017/ 5 | uri: <%= ENV.fetch('MONGODB_URI') %> 6 | options: 7 | server_selection_timeout: 1 -------------------------------------------------------------------------------- /lib/fana_os.rb: -------------------------------------------------------------------------------- 1 | require 'dotenv/load' 2 | require_relative 'fana_os/pocket_utils' 3 | require_relative 'fana_os/obsidian_utils' 4 | 5 | module FanaOs 6 | def self.configure_mongo 7 | Mongoid.load!(File.join(File.dirname(__FILE__), 'config', 'mongoid.yml')) 8 | end 9 | 10 | def self.configure_pocket 11 | Pocket.configure do |config| 12 | config.consumer_key = ENV.fetch('POCKET_CONSUMER_KEY') 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/fana_os/obsidian_utils.rb: -------------------------------------------------------------------------------- 1 | require 'git' 2 | 3 | module FanaOs 4 | class ObsidianUtils 5 | def self.push_git 6 | git = Git.open(ENV.fetch('OBSIDIAN_ROOT')) 7 | git.add(all: true) 8 | git.commit_all("Stash Obsidian on #{DateTime.current}") 9 | git.push 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/fana_os/pocket_utils.rb: -------------------------------------------------------------------------------- 1 | require 'pocket-ruby' 2 | require 'mongoid' 3 | 4 | module FanaOs 5 | class PocketUtils 6 | def initialize 7 | FanaOs.configure_mongo 8 | FanaOs.configure_pocket 9 | 10 | @client = Pocket.client(access_token: ENV.fetch('POCKET_ACCESS_TOKEN')) 11 | end 12 | 13 | def retrieve 14 | pocket_articles = @client.retrieve( 15 | images: 1, 16 | videos: 1, 17 | tags: 1, 18 | rediscovery: 1, 19 | annotations: 1, 20 | authors: 1, 21 | itemOptics: 1, 22 | meta: 1, 23 | posts: 1, 24 | total: 1, 25 | forceaccount: 1, 26 | state: 'all', 27 | sort: 'newest', 28 | detailType: 'complete' 29 | )['list'].values 30 | 31 | pocket_articles.each { |article| process_article(article) } 32 | 33 | pocket_articles 34 | end 35 | 36 | def process_article(attributes) 37 | article = FanaOs::PocketArticle.find_or_create_by( 38 | pocket_id: attributes['item_id'], 39 | title: attributes['resolved_title'], 40 | url: attributes['resolved_url'], 41 | added_at: Time.at(attributes['time_added'].to_i) 42 | ) 43 | 44 | # 0 = unread / 1 = read. We don't want to add highlights multiple times. 45 | if attributes['status'].to_i == 1 46 | time_read = Time.at(attributes['time_read'].to_i) 47 | 48 | notes_path = "#{ENV.fetch('OBSIDIAN_ROOT')}/research/Daily/#{time_read.strftime('%Y-%m-%d')}.md" 49 | 50 | annotations = [] 51 | 52 | highlights = attributes['annotations'].to_a.map do |a| 53 | annotation = FanaOs::PocketAnnotation.find_or_create_by( 54 | annotation_id: a['annotation_id'], 55 | quote: a['quote'], 56 | patch: a['patch'], 57 | pocket_article_id: a['item_id'], 58 | annotated_at: Date.parse(a['created_at']) 59 | ) 60 | 61 | annotations << annotation 62 | 63 | "- #{a['quote']}" 64 | end 65 | 66 | content_to_append = [ 67 | "**[#{attributes['resolved_title']}](#{attributes['resolved_url']})**", 68 | "*By #{attributes['authors'].to_h.values.map { |author| author['name'] }.join(', ')}*", 69 | "Tags: #{attributes['tags'].to_h.keys.map { |key| "[[#{key}]]" }.join(', ')}", 70 | highlights, 71 | "\n", 72 | "\n" 73 | ].flatten.join("\n") 74 | 75 | # Making sure we don't add notes twice 76 | if !File.exist?(notes_path) || File.readlines(notes_path).any?{ |l| l.include? attributes['resolved_title'] } 77 | File.write(notes_path, content_to_append, mode: 'a') 78 | end 79 | 80 | article.update( 81 | read_at: time_read, 82 | word_count: attributes['word_count'], 83 | annotations_added_at: Date.current, 84 | tags: attributes['tags'].to_h.keys 85 | ) 86 | end 87 | end 88 | end 89 | 90 | class PocketArticle 91 | include Mongoid::Document 92 | 93 | field :pocket_id, type: String 94 | field :title, type: String 95 | field :url, type: String 96 | field :added_at, type: Date 97 | field :read_at, type: Date 98 | field :word_count, type: Integer 99 | field :annotations_added_at, type: Date 100 | field :tags, type: Array, default: [] 101 | 102 | has_many :pocket_annotations 103 | end 104 | 105 | class PocketAnnotation 106 | include Mongoid::Document 107 | 108 | field :annotation_id, type: String 109 | field :pocket_article_id, type: String 110 | field :quote, type: String 111 | field :patch, type: String 112 | field :annotated_at, type: Date 113 | 114 | belongs_to :pocket_article 115 | end 116 | end -------------------------------------------------------------------------------- /oauth_server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'dotenv/load' 3 | require 'pocket-ruby' 4 | 5 | enable :sessions 6 | 7 | CALLBACK_URL = 'http://localhost:4567/oauth/callback'.freeze 8 | 9 | Pocket.configure do |config| 10 | config.consumer_key = ENV['POCKET_CONSUMER_KEY'] 11 | end 12 | 13 | get '/reset' do 14 | puts "GET /reset" 15 | session.clear 16 | end 17 | 18 | get "/" do 19 | puts "GET /" 20 | puts "session: #{session}" 21 | 22 | if session[:access_token] 23 | session[:access_token] 24 | else 25 | 'Connect with Pocket' 26 | end 27 | end 28 | 29 | get "/oauth/connect" do 30 | session[:code] = Pocket.get_code(:redirect_uri => CALLBACK_URL) 31 | new_url = Pocket.authorize_url(:code => session[:code], :redirect_uri => CALLBACK_URL) 32 | puts "Code: #{session[:code]}" 33 | redirect new_url 34 | end 35 | 36 | get "/oauth/callback" do 37 | puts "OAUTH CALLBACK" 38 | puts "request.url: #{request.url}" 39 | puts "request.body: #{request.body.read}" 40 | result = Pocket.get_result(session[:code], :redirect_uri => CALLBACK_URL) 41 | session[:access_token] = result['access_token'] 42 | puts result['access_token'] 43 | puts result['username'] 44 | # Alternative method to get the access token directly 45 | #session[:access_token] = Pocket.get_access_token(session[:code]) 46 | puts session[:access_token] 47 | puts "session: #{session}" 48 | redirect "/" 49 | end --------------------------------------------------------------------------------