commit c5fe900f96fcb6fac170c3195dbe69116e8a77da Author: Nemo Date: Fri Jun 1 00:56:39 2018 +0530 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b453d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +.env diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..a395d3b --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,4 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + TargetRubyVersion: 2.5 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..16335d2 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,30 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2018-06-01 00:51:49 +0530 using RuboCop version 0.56.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +Metrics/AbcSize: + Max: 24 + +# Offense count: 2 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 14 + +# Offense count: 2 +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'app.rb' + - 'opml.rb' + +# Offense count: 2 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 87 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5140b60 --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'nokogiri', '~> 1.8' +gem 'octokit', '~> 4.0' +gem 'redis', '~> 4.0' +gem 'sinatra', '~> 2.0' +gem 'sinatra-contrib', '~> 2.0' +gem 'dotenv' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..77671d3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,52 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + backports (3.11.3) + dotenv (2.4.0) + faraday (0.15.2) + multipart-post (>= 1.2, < 3) + mini_portile2 (2.3.0) + multi_json (1.13.1) + multipart-post (2.0.0) + mustermann (1.0.2) + nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) + octokit (4.9.0) + sawyer (~> 0.8.0, >= 0.5.3) + public_suffix (3.0.2) + rack (2.0.5) + rack-protection (2.0.1) + rack + redis (4.0.1) + sawyer (0.8.1) + addressable (>= 2.3.5, < 2.6) + faraday (~> 0.8, < 1.0) + sinatra (2.0.1) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.1) + tilt (~> 2.0) + sinatra-contrib (2.0.1) + backports (>= 2.0) + multi_json + mustermann (~> 1.0) + rack-protection (= 2.0.1) + sinatra (= 2.0.1) + tilt (>= 1.3, < 3) + tilt (2.0.8) + +PLATFORMS + ruby + +DEPENDENCIES + dotenv + nokogiri (~> 1.8) + octokit (~> 4.0) + redis (~> 4.0) + sinatra (~> 2.0) + sinatra-contrib (~> 2.0) + +BUNDLED WITH + 1.16.2 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..6cfa2d1 --- /dev/null +++ b/app.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true +require 'dotenv/load' +require 'sinatra/base' +require 'sinatra/reloader' +require 'octokit' +require 'redis' +require 'json' +require './opml' + +Octokit.auto_paginate = true + +class MyApp < Sinatra::Base + configure :development do + register Sinatra::Reloader + also_reload 'opml.rb' + end + configure do + set :r, Redis.new + set :client, Octokit::Client.new( + client_id: ENV.fetch('GITHUB_CLIENT_ID'), + client_secret: ENV.fetch('GITHUB_CLIENT_SECRET'), + per_page: 100, + auto_traversal: true, + auto_paginate: true + ) + end + + def get_starred_repos_from_cache(user) + from_cache = settings.r.get "#{user}.repos" + if from_cache + time = settings.r.get "#{user}.repos.time" || Time.now.to_i + return [JSON.parse(from_cache), time] + end + repos = [] + starred = settings.client.starred(user) + + starred.each do |repo| + repos.push repo[:full_name] + end + + time = Time.now.to_i + settings.r.set "#{user}.repos", repos + settings.r.set "#{user}.repos.time", time + [repos, time] + end + + get '/github/:user/starred.opml' do + user = params[:user] + repos, time = get_starred_repos_from_cache(user) + opml = OPML.new do + @title = "Releases: #{user}/starred" + @date_created = Time.new(time.to_i).rfc822 + @date_modified = Time.now.rfc822 + @owner_name = user + end + repos.each do |r| + title = "Release notes from #{r}" + html_url = "https://github.com/#{r}/releases" + rss_url = "#{html_url}.atom" + opml.add_outline( + text: title, description: "#{r}/releases", html_url: html_url, + xml_url: rss_url, title: title, + type: 'rss', version: 'RSS2' + ) + end + + filename = "#{user}-starred-releases.opml" + + response.headers['Content-Disposition'] = "attachment; filename=#{filename};" + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['Content-Transfer-Encoding'] = 'binary' + + opml.xml + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..5e81637 --- /dev/null +++ b/config.ru @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require './app' +run MyApp diff --git a/opml.rb b/opml.rb new file mode 100644 index 0000000..626d997 --- /dev/null +++ b/opml.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'nokogiri' + +class OPML + PROPERTIES = %w[title date_created date_modified + owner_name owner_email docs].freeze + + attr_accessor :title, :date_created, :date_modified, :owner_name, :owner_email, :docs + + def initialize(&block) + instance_eval(&block) + @docs = 'http://dev.opml.org/spec2.html' + @outlines = [] + end + + def camel_case(str) + str.downcase.split('_').each_with_index.map do |v, i| + i.zero? ? v : v.capitalize + end.join + end + + def add_outline(params) + params = Hash[params.map { |k, v| [camel_case(k.to_s), v] }] + @outlines.push params + end + + def add_note_with_params(xml, attrs) + attrs.each do |attr| + val = send(attr.to_sym) + key = camel_case(attr).to_sym + xml.send(key, val) if val + end + end + + def xml + Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.opml(version: '2.0') do + xml.head do + add_note_with_params xml, PROPERTIES + end + + xml.body do + xml.outline text: 'GitHub Releases' do + @outlines.each do |outline| + xml.outline outline + end + end + end + end + end.to_xml + end +end