Handle duplicated api key names in transfer job (#222)

This commit is contained in:
Max Wofford 2025-05-12 17:39:45 -04:00 committed by GitHub
parent a7da9fd8fd
commit ce04f80b47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 228 additions and 1 deletions

View 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

View file

@ -0,0 +1,7 @@
class WakatimeMirrorSyncJob < ApplicationJob
queue_as :default
def perform(mirror)
mirror.sync_heartbeats
end
end

View file

@ -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

View file

@ -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

View 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

View 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>

View file

@ -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>

View file

@ -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

View 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
View file

@ -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