twinkle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8ac554f1dff3ef39f4915f981ec076b50702272d4e580fd53c883065b01c430
4
+ data.tar.gz: dc5f679b7366c31e316f067886298f5408b32d11706bdd56de76a95ad6550685
5
+ SHA512:
6
+ metadata.gz: 37eaf8576e4d1b47332eb5fd48c082cb08b75df2578bba102308d5d74e550e3964bbae168a462297e4af2b78f9f78d5a9f0cbe1c2041cc9236f38b13d8ebeaa2
7
+ data.tar.gz: 9fcf89b14a7a6b945fe607af393db115f79d28b29a4d33585844b82009455103871060dffcc41e0017ef597ef6a05a92091410fd012fc30964fb7d1156a25181
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2024 Tim Marks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Twinkle
2
+
3
+ Twinkle makes it easy to serve up an appcast xml feed and to store anonymized and aggregated Sparkle Framework statistics.
4
+
5
+ ## Usage
6
+
7
+ You can easily mount Twinkle to manage and serve your Appcast and store and summarize statistics.
8
+
9
+ Twinkle contains 5 models, an appcast controller and a summarize concern for creating aggregate usage statistics.
10
+
11
+ Too much trouble hosting your own? Check out [appable.xyz](https://appable.xyz)
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem "twinkle"
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ ```bash
24
+ $ bundle
25
+ ```
26
+
27
+ Or install it yourself as:
28
+
29
+ ```bash
30
+ $ gem install twinkle
31
+ ```
32
+
33
+ ## Install the migrations
34
+
35
+ ```bash
36
+ bin/rails twinkle:install:migrations
37
+ ```
38
+
39
+ ## Mount the routes
40
+
41
+ Add the folllwing to your config/routes
42
+
43
+ ```
44
+ mount Twinkle::Engine => "/"
45
+ ```
46
+
47
+ This will mount the appcast routes at /updates/:app.slug
48
+
49
+ ## Contributing
50
+
51
+ Pull requests welcome.
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/twinkle .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,25 @@
1
+ module Twinkle
2
+ class AppcastController < ApplicationController
3
+ def show
4
+ @app = App.with_versions.find_by!(slug: params[:slug])
5
+ Event.create(app: @app, **event_params)
6
+ render layout: false, formats: :xml
7
+ end
8
+
9
+ private
10
+
11
+ def event_params
12
+ params.permit(
13
+ :cpu64bit,
14
+ :ncpu,
15
+ :appVersion,
16
+ :cpuFreqMHz,
17
+ :cputype,
18
+ :cpusubtype,
19
+ :ramMB,
20
+ :osVersion,
21
+ :lang
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module Twinkle
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twinkle
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twinkle
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Twinkle
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,44 @@
1
+ module Summarize
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ def summarize_events(period, start_date, end_date)
6
+ # Find or create a summary for the week
7
+ summary = Twinkle::Summary.with_datapoints.find_or_create_by(app: self, period: period, start_date: start_date, end_date: end_date)
8
+ data_hash = get_data_hash(start_date, end_date)
9
+ datapoints = get_datapoints(summary, data_hash)
10
+ save_summary(datapoints)
11
+ end
12
+
13
+ def get_data_hash(start_date, end_date)
14
+ data_hash = Twinkle::Summary.empty_datapoints_hash()
15
+
16
+ # We're going to accumulate the data points for the summary
17
+ events.created_between(start_date, end_date).find_each do |event|
18
+ data_hash['users']['sessions'] = (data_hash.dig('users', 'sessions') || 0) + 1
19
+ Twinkle::Event.fields.each do |field|
20
+ data_hash[field][event[field]] = (data_hash.dig(field, event[field]) || 0) + 1
21
+ end
22
+ end
23
+ data_hash
24
+ end
25
+
26
+ def get_datapoints(summary, data_hash)
27
+ datapoints = []
28
+ data_hash.each do |name, values|
29
+ values.each do |value, count|
30
+ datapoints << {twinkle_summary_id: summary.id, name: name, value: value, count: count}
31
+ end
32
+ end
33
+ datapoints
34
+ end
35
+
36
+ def save_summary(datapoints)
37
+ Twinkle::Datapoint.upsert_all(
38
+ datapoints,
39
+ unique_by: [:twinkle_summary_id, :name, :value],
40
+ update_only: [:count]
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ module Twinkle
2
+ class App < ApplicationRecord
3
+ include Summarize
4
+
5
+ has_many :versions, foreign_key: 'twinkle_app_id', class_name: 'Twinkle::Version'
6
+ has_many :events, foreign_key: 'twinkle_app_id', class_name: 'Twinkle::Event'
7
+ has_many :summaries, foreign_key: 'twinkle_app_id', class_name: 'Twinkle::Summary'
8
+ has_one_attached :icon
9
+
10
+ scope :with_versions, -> { includes(:versions).order('twinkle_versions.build desc') }
11
+ scope :with_latest_version, -> { includes(:versions).order('twinkle_versions.build desc').limit(1) }
12
+ scope :with_latest_summary, -> { includes(:summaries).order('twinkle_summaries.created_at desc').limit(1) }
13
+ scope :with_summaries_since, ->(date) { includes(:summaries).where('twinkle_summaries.start >= ?', date).order('twinkle_summaries.start asc') }
14
+
15
+ validates :name, presence: true
16
+ validates :slug, presence: true, uniqueness: true
17
+ validates :description, presence: true
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Twinkle
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Twinkle
2
+ class Datapoint < ApplicationRecord
3
+ belongs_to :summary, foreign_key: :twinkle_summary_id, class_name: 'Twinkle::Summary'
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Twinkle
2
+ class Event < ApplicationRecord
3
+ belongs_to :app, foreign_key: 'twinkle_app_id', class_name: 'Twinkle::App'
4
+
5
+ scope :created_between, -> (start_date, end_date) {where("created_at >= ? AND created_at <= ?", start_date, end_date )}
6
+
7
+ alias_attribute :app_version, :version
8
+ alias_attribute :cpuFreqMHz, :cpu_freq_mhz
9
+ alias_attribute :osVersion, :os_version
10
+
11
+ def self.fields
12
+ attribute_names.select{ |name| !['id', 'twinkle_app_id', 'created_at', 'updated_at'].include?(name) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module Twinkle
2
+ class Summary < ApplicationRecord
3
+ belongs_to :app, foreign_key: :twinkle_app_id, class_name: 'Twinkle::App'
4
+ has_many :datapoints, foreign_key: :twinkle_summary_id, class_name: 'Twinkle::Datapoint'
5
+
6
+ scope :week_of, -> (start_date, end_date) { where(period: :week).where("start_date >= ? AND end_date <= ?", start_date, end_date) }
7
+ scope :with_datapoints, -> { includes(:datapoints) }
8
+
9
+ enum period: { week: 0, fortnight: 1, month: 2 }
10
+
11
+ def hash_datapoints
12
+ data = Event.fields.map { |name| [name, {}] }.append(['users', {}]).to_h
13
+ datapoints.each do |datapoint|
14
+ data[datapoint.name][datapoint.value] = datapoint.count
15
+ end
16
+ data
17
+ end
18
+
19
+ def self.empty_datapoints_hash
20
+ Event.fields.map { |name| [name, {}] }.append(['users', {}]).to_h
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ require 'uri'
2
+
3
+ module Twinkle
4
+ class Version < ApplicationRecord
5
+ belongs_to :app, foreign_key: 'twinkle_app_id', class_name: 'Twinkle::App'
6
+
7
+ validates :number, presence: true
8
+ validates :build, presence: true
9
+ validates :description, presence: true
10
+ validates :binary_url, presence: true
11
+ validates :length, presence: true
12
+ validates :length, numericality: { only_integer: true, greater_than: 0 }
13
+
14
+ # validates that the binary_url is a valid URL
15
+ validate :binary_url_is_url
16
+
17
+ # validates that one of the two signatures is present
18
+ validate :signature_present
19
+
20
+ private
21
+
22
+ def binary_url_is_url
23
+ begin
24
+ uri = URI.parse(binary_url)
25
+ raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) && uri.host.present?
26
+ rescue URI::InvalidURIError
27
+ errors.add(:binary_url, "is not a valid URL")
28
+ end
29
+ end
30
+
31
+ def signature_present
32
+ if dsa_signature.blank? && ed_signature.blank?
33
+ errors.add(:base, "At least one of the two signatures must be present")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Twinkle</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "twinkle/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" standalone="yes"?>
2
+ <rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
3
+ <channel>
4
+ <title><%= @app.name %></title>
5
+ <% @app.versions.each do |version| %>
6
+ <item>
7
+ <title><%= version.number %></title>
8
+ <pubDate><%= version.inserted_at %></pubDate>
9
+ <sparkle:version><%= version.build %></sparkle:version>
10
+ <sparkle:shortVersionString><%= version.number %></sparkle:shortVersionString>
11
+ <sparkle:minimumSystemVersion><%= version.min_system_version %></sparkle:minimumSystemVersion>
12
+ <description>
13
+ <%= version.description %>
14
+ </description>
15
+ <enclosure url="<%= version.url %>" length="<%= version.length %>" type="application/octet-stream" sparkle:dsaSignature="<%= version.dsa_signature %>" sparkle:edSignature="<%= version.ed_signature %>"/>
16
+ </item>
17
+ <% end %>
18
+ </channel>
19
+ </rss>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Twinkle::Engine.routes.draw do
2
+ get 'updates/:slug' => 'appcast#show'
3
+ end
@@ -0,0 +1,12 @@
1
+ class CreateTwinkleApps < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :twinkle_apps do |t|
4
+ t.string :name
5
+ t.string :slug
6
+ t.string :description
7
+
8
+ t.timestamps
9
+ end
10
+ add_index :twinkle_apps, :slug, unique: true
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ class CreateTwinkleVersions < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :twinkle_versions do |t|
4
+ t.references :twinkle_app, null: false, foreign_key: true
5
+ t.string :number
6
+ t.string :build
7
+ t.string :description
8
+ t.string :binary_url
9
+ t.string :dsa_signature
10
+ t.string :ed_signature
11
+ t.string :length
12
+
13
+ t.timestamps
14
+ end
15
+ add_index :twinkle_versions, :build
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ class CreateTwinkleEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :twinkle_events do |t|
4
+ t.references :twinkle_app, null: false, foreign_key: true
5
+ t.string :version
6
+ t.boolean :cpu64bit
7
+ t.integer :ncpu
8
+ t.string :cpu_freq_mhz
9
+ t.string :cputype
10
+ t.string :cpusubtype
11
+ t.string :model
12
+ t.string :ram_mb
13
+ t.string :os_version
14
+ t.string :lang
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ class CreateTwinkleSummaries < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :twinkle_summaries do |t|
4
+ t.references :twinkle_app, null: false, foreign_key: true
5
+ t.integer :period
6
+ t.datetime :start_date
7
+ t.datetime :end_date
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :twinkle_summaries, [:period, :start_date, :end_date]
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateTwinkleDatapoints < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :twinkle_datapoints do |t|
4
+ t.references :twinkle_summary, null: false, foreign_key: true
5
+ t.string :name
6
+ t.string :value
7
+ t.integer :count
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :twinkle_datapoints, [:twinkle_summary_id, :name, :value], unique: true
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # desc "Explaining what the task does"
2
+ # task :twinkle do
3
+ # # Task goes here
4
+ # end
5
+
6
+ desc "Summarize Twinkle Events in the week period"
7
+ task :summarize_twinkle_events_week do
8
+ week_start = Time.now.beginning_of_week
9
+ week_end = Time.now.end_of_week
10
+
11
+ Apps.all.each do |app|
12
+ app.summarize_events(Summary.period[:week], week_start, week_end)
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Twinkle
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Twinkle
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Twinkle
2
+ VERSION = "0.1.0"
3
+ end
data/lib/twinkle.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "twinkle/version"
2
+ require "twinkle/engine"
3
+
4
+ module Twinkle
5
+ # Your code goes here...
6
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twinkle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Marks
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0.0
27
+ description: Twinkle is a Rails engine for hosting appcast and collecting anonymous
28
+ sparkle-project usage data.
29
+ email:
30
+ - t@imothee.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - app/assets/config/twinkle_manifest.js
39
+ - app/assets/stylesheets/twinkle/application.css
40
+ - app/controllers/twinkle/appcast_controller.rb
41
+ - app/controllers/twinkle/application_controller.rb
42
+ - app/helpers/twinkle/application_helper.rb
43
+ - app/jobs/twinkle/application_job.rb
44
+ - app/mailers/twinkle/application_mailer.rb
45
+ - app/models/concerns/summarize.rb
46
+ - app/models/twinkle/app.rb
47
+ - app/models/twinkle/application_record.rb
48
+ - app/models/twinkle/datapoint.rb
49
+ - app/models/twinkle/event.rb
50
+ - app/models/twinkle/summary.rb
51
+ - app/models/twinkle/version.rb
52
+ - app/views/layouts/twinkle/application.html.erb
53
+ - app/views/twinkle/appcast/show.xml.erb
54
+ - config/routes.rb
55
+ - db/migrate/20240517231834_create_twinkle_apps.rb
56
+ - db/migrate/20240517232121_create_twinkle_versions.rb
57
+ - db/migrate/20240517233457_create_twinkle_events.rb
58
+ - db/migrate/20240517234016_create_twinkle_summaries.rb
59
+ - db/migrate/20240518000405_create_twinkle_datapoints.rb
60
+ - lib/tasks/twinkle_tasks.rake
61
+ - lib/twinkle.rb
62
+ - lib/twinkle/engine.rb
63
+ - lib/twinkle/version.rb
64
+ homepage: https://github.com/imothee/twinkle
65
+ licenses: []
66
+ metadata:
67
+ allowed_push_host: https://rubygems.org/
68
+ homepage_uri: https://github.com/imothee/twinkle
69
+ source_code_uri: https://github.com/imothee/twinkle
70
+ changelog_uri: https://github.com/imothee/twinkle/blob/main/CHANGELOG.md
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.3.26
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Twinkle is a Rails engine for hosting appcast and collecting anonymous sparkle-project
90
+ usage data.
91
+ test_files: []