mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 22:15:14 +00:00
Handle duplicated api key names in transfer job (#222)
This commit is contained in:
parent
a7da9fd8fd
commit
ce04f80b47
10 changed files with 228 additions and 1 deletions
39
app/controllers/wakatime_mirrors_controller.rb
Normal file
39
app/controllers/wakatime_mirrors_controller.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class WakatimeMirrorsController < ApplicationController
|
||||
before_action :set_user
|
||||
before_action :require_current_user
|
||||
before_action :set_mirror, only: [ :destroy ]
|
||||
|
||||
def create
|
||||
@mirror = @user.wakatime_mirrors.build(mirror_params)
|
||||
if @mirror.save
|
||||
redirect_to my_settings_path, notice: "WakaTime mirror added successfully"
|
||||
else
|
||||
redirect_to my_settings_path, alert: "Failed to add WakaTime mirror: #{@mirror.errors.full_messages.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@mirror.destroy
|
||||
redirect_to my_settings_path, notice: "WakaTime mirror removed successfully"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:user_id])
|
||||
end
|
||||
|
||||
def set_mirror
|
||||
@mirror = @user.wakatime_mirrors.find(params[:id])
|
||||
end
|
||||
|
||||
def mirror_params
|
||||
params.require(:wakatime_mirror).permit(:endpoint_url, :encrypted_api_key)
|
||||
end
|
||||
|
||||
def require_current_user
|
||||
unless @user == current_user
|
||||
redirect_to root_path, alert: "You are not authorized to access this page"
|
||||
end
|
||||
end
|
||||
end
|
||||
7
app/jobs/wakatime_mirror_sync_job.rb
Normal file
7
app/jobs/wakatime_mirror_sync_job.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class WakatimeMirrorSyncJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(mirror)
|
||||
mirror.sync_heartbeats
|
||||
end
|
||||
end
|
||||
|
|
@ -87,9 +87,12 @@ class Heartbeat < ApplicationRecord
|
|||
self.inheritance_column = nil
|
||||
|
||||
belongs_to :user
|
||||
has_many :wakatime_mirrors, dependent: :destroy
|
||||
|
||||
validates :time, presence: true
|
||||
|
||||
after_create :mirror_to_wakatime
|
||||
|
||||
def self.recent_count
|
||||
Cache::HeartbeatCountsJob.perform_now[:recent_count]
|
||||
end
|
||||
|
|
@ -128,4 +131,8 @@ class Heartbeat < ApplicationRecord
|
|||
self.fields_hash = self.class.generate_fields_hash(self.attributes)
|
||||
end
|
||||
end
|
||||
|
||||
def mirror_to_wakatime
|
||||
WakatimeMirror.mirror_heartbeat(self)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class User < ApplicationRecord
|
|||
primary_key: :slack_uid,
|
||||
class_name: "SailorsLog"
|
||||
|
||||
has_many :wakatime_mirrors, dependent: :destroy
|
||||
|
||||
def streak_days
|
||||
@streak_days ||= heartbeats.daily_streaks_for_users([ id ]).values.first
|
||||
end
|
||||
|
|
|
|||
77
app/models/wakatime_mirror.rb
Normal file
77
app/models/wakatime_mirror.rb
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
class WakatimeMirror < ApplicationRecord
|
||||
belongs_to :user
|
||||
has_many :heartbeats, through: :user
|
||||
|
||||
encrypts :encrypted_api_key, deterministic: false
|
||||
|
||||
validates :endpoint_url, presence: true
|
||||
validates :encrypted_api_key, presence: true
|
||||
validates :endpoint_url, uniqueness: { scope: :user_id }
|
||||
|
||||
after_create :schedule_initial_sync
|
||||
|
||||
def unsynced_heartbeats
|
||||
# For testing: sync the 100 most recent heartbeats
|
||||
heartbeats.order(time: :desc).limit(100)
|
||||
end
|
||||
|
||||
def sync_heartbeats
|
||||
return unless encrypted_api_key.present?
|
||||
|
||||
# Get the next batch of heartbeats to sync (max 25 per WakaTime API limit)
|
||||
batch = unsynced_heartbeats.limit(25).to_a
|
||||
return if batch.empty?
|
||||
|
||||
# Print timestamps of heartbeats being synced
|
||||
puts "\nSyncing heartbeats:"
|
||||
batch.each do |h|
|
||||
puts " #{Time.at(h.time).strftime('%Y-%m-%d %H:%M:%S')} - #{h.entity}"
|
||||
end
|
||||
puts ""
|
||||
|
||||
# Send them all in a single request using the bulk endpoint
|
||||
begin
|
||||
response = HTTP.headers(
|
||||
"Authorization" => "Basic #{Base64.strict_encode64(encrypted_api_key + ':')}",
|
||||
"Content-Type" => "application/json"
|
||||
).post(
|
||||
"#{endpoint_url}/users/current/heartbeats.bulk",
|
||||
json: batch.map { |h| h.attributes.slice(
|
||||
:branch,
|
||||
:category,
|
||||
:dependencies,
|
||||
:editor,
|
||||
:entity,
|
||||
:language,
|
||||
:machine,
|
||||
:operating_system,
|
||||
:project,
|
||||
:type,
|
||||
:user_agent,
|
||||
:line_additions,
|
||||
:line_deletions,
|
||||
:lineno,
|
||||
:lines,
|
||||
:cursorpos,
|
||||
:project_root_count,
|
||||
:time,
|
||||
:is_write
|
||||
) }
|
||||
)
|
||||
|
||||
if response.status.success?
|
||||
update_column(:last_synced_at, Time.current)
|
||||
else
|
||||
Rails.logger.error("Failed to sync heartbeats to #{endpoint_url}: #{response.body}")
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("Error syncing heartbeats to #{endpoint_url}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_initial_sync
|
||||
WakatimeMirrorSyncJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
32
app/views/users/_wakatime_mirror_section.html.erb
Normal file
32
app/views/users/_wakatime_mirror_section.html.erb
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<section>
|
||||
<h2 id="wakatime_mirror">WakaTime Mirror</h2>
|
||||
<p>Mirror your coding activity to WakaTime.</p>
|
||||
|
||||
<% if current_user.wakatime_mirrors.any? %>
|
||||
<div class="mirrors-list">
|
||||
<% current_user.wakatime_mirrors.each do |mirror| %>
|
||||
<div class="mirror-item">
|
||||
<p>
|
||||
<strong>Endpoint:</strong> <%= mirror.endpoint_url %><br>
|
||||
<strong>Last synced:</strong> <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + " ago" : "Never" %>
|
||||
</p>
|
||||
<%= button_to "Delete", user_wakatime_mirror_path(current_user, mirror), method: :delete, class: "button", data: { confirm: "Are you sure?" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with(model: [current_user, WakatimeMirror.new], local: true) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label :endpoint_url, "WakaTime API Endpoint" %>
|
||||
<%= f.text_field :endpoint_url, value: "https://wakatime.com/api/v1", placeholder: "https://wakatime.com/api/v1" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :encrypted_api_key, "WakaTime API Key" %>
|
||||
<%= f.password_field :encrypted_api_key, placeholder: "Enter your WakaTime API key" %>
|
||||
</div>
|
||||
|
||||
<%= f.submit "Add Mirror", class: "button" %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
|
@ -212,6 +212,41 @@
|
|||
</p>
|
||||
</section>
|
||||
|
||||
<% admin_tool do %>
|
||||
<section>
|
||||
<h2 id="wakatime_mirror">WakaTime Mirror</h2>
|
||||
<p>Mirror your coding activity to WakaTime.</p>
|
||||
|
||||
<% if current_user.wakatime_mirrors.any? %>
|
||||
<div class="mirrors-list">
|
||||
<% current_user.wakatime_mirrors.each do |mirror| %>
|
||||
<div class="mirror-item">
|
||||
<p>
|
||||
<strong>Endpoint:</strong> <%= mirror.endpoint_url %><br>
|
||||
<strong>Last synced:</strong> <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + " ago" : "Never" %>
|
||||
</p>
|
||||
<%= button_to "Delete", user_wakatime_mirror_path(current_user, mirror), method: :delete, class: "button", data: { confirm: "Are you sure?" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with(model: [current_user, WakatimeMirror.new], local: true) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label :endpoint_url, "WakaTime API Endpoint" %>
|
||||
<%= f.text_field :endpoint_url, value: "https://wakatime.com/api/v1", placeholder: "https://wakatime.com/api/v1" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :encrypted_api_key, "WakaTime API Key" %>
|
||||
<%= f.text_field :encrypted_api_key, placeholder: "Enter your WakaTime API key" %>
|
||||
</div>
|
||||
|
||||
<%= f.submit "Add Mirror", class: "button" %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<h2 id="user_migration_assistant">Migration assistant</h2>
|
||||
<p>This will migrate your heartbeats from waka.hackclub.com to this platform.</p>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ Rails.application.routes.draw do
|
|||
member do
|
||||
patch :update_trust_level
|
||||
end
|
||||
resource :wakatime_mirrors, only: [ :create ]
|
||||
resources :wakatime_mirrors, only: [ :destroy ]
|
||||
end
|
||||
|
||||
get "my/projects", to: "my/project_repo_mappings#index", as: :my_projects
|
||||
|
|
|
|||
14
db/migrate/20250512205858_create_wakatime_mirrors.rb
Normal file
14
db/migrate/20250512205858_create_wakatime_mirrors.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class CreateWakatimeMirrors < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :wakatime_mirrors do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.string :endpoint_url, null: false, default: "https://wakatime.com/api/v1"
|
||||
t.string :encrypted_api_key, null: false
|
||||
t.datetime :last_synced_at
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :wakatime_mirrors, [ :user_id, :endpoint_url ], unique: true
|
||||
end
|
||||
end
|
||||
14
db/schema.rb
generated
14
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_05_09_191155) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_05_12_205858) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
|
@ -344,6 +344,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_09_191155) do
|
|||
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
|
||||
end
|
||||
|
||||
create_table "wakatime_mirrors", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "endpoint_url", default: "https://wakatime.com/api/v1", null: false
|
||||
t.string "encrypted_api_key", null: false
|
||||
t.datetime "last_synced_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id", "endpoint_url"], name: "index_wakatime_mirrors_on_user_id_and_endpoint_url", unique: true
|
||||
t.index ["user_id"], name: "index_wakatime_mirrors_on_user_id"
|
||||
end
|
||||
|
||||
add_foreign_key "api_keys", "users"
|
||||
add_foreign_key "email_addresses", "users"
|
||||
add_foreign_key "email_verification_requests", "users"
|
||||
|
|
@ -353,4 +364,5 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_09_191155) do
|
|||
add_foreign_key "mailing_addresses", "users"
|
||||
add_foreign_key "project_repo_mappings", "users"
|
||||
add_foreign_key "sign_in_tokens", "users"
|
||||
add_foreign_key "wakatime_mirrors", "users"
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue