First version of bulk uploads

This commit is contained in:
Max Wofford
2025-03-03 23:13:39 -05:00
parent 27b71a8c83
commit 10c5cc5bbc
9 changed files with 197 additions and 53 deletions

View File

@@ -0,0 +1,137 @@
class Api::Hackatime::V1::HackatimeController < ApplicationController
before_action :set_user, except: [ :index ]
# before_action :set_json_format
skip_before_action :verify_authenticity_token
def index
redirect_to root_path
end
def push_heartbeats
puts "Raw params: #{params.inspect}"
puts "Creating heartbeat with params: #{heartbeat_params}"
attrs = heartbeat_params.merge({ user: @user })
puts "Merged attrs: #{attrs}"
new_heartbeat = Heartbeat.new(attrs)
if new_heartbeat.save!
render json: { responses: [ { heartbeat: new_heartbeat.attributes, status: 201 } ] }, status: :created
else
render json: { error: "Failed to create heartbeat: #{new_heartbeat.errors.full_messages}" }, status: :unprocessable_entity
end
end
def push_heartbeats_bulk
new_heartbeats = []
ActiveRecord::Base.transaction do
new_heartbeats = heartbeat_bulk_params.map do |heartbeat|
attrs = heartbeat.merge({ user_id: @user.id })
Heartbeat.create(attrs)
end
end
responses = []
new_heartbeats.each do |heartbeat|
responses << [ heartbeat.attributes, heartbeat.persisted? ? 201 : 422 ]
end
render json: { responses: responses }, status: :success
end
def status_bar_today
hbt = @user.heartbeats.today
render json: {
"data": {
"grand_total": {
"decimal": "yolo",
"digital": "wahoo",
"hours": hbt.duration_seconds / 3600,
"minutes": (hbt.duration_seconds % 3600) / 60,
"text": @user.format_extension_text(hbt.duration_seconds),
"total_seconds": hbt.duration_seconds
},
"categories": hbt.distinct.pluck(:category),
"dependencies": hbt.distinct.pluck(:dependencies),
"editors": hbt.distinct.pluck(:editor),
"languages": hbt.distinct.pluck(:language),
"machines": hbt.distinct.pluck(:machine),
"operating_systems": hbt.distinct.pluck(:operating_system),
"projects": hbt.distinct.pluck(:project),
"range": {
"text": "Today",
"timezone": "UTC"
}
}
}
end
private
# def set_json_format
# request.format = :json
# end
def set_user
api_header = request.headers["Authorization"]
raw_token = api_header&.split(" ")&.last
header_type = api_header&.split(" ")&.first
if header_type == "Bearer"
api_token = raw_token
elsif header_type == "Basic"
api_token = Base64.decode64(raw_token)
end
return render json: { error: "Unauthorized" }, status: :unauthorized unless api_token.present?
valid_key = ApiKey.find_by(token: api_token)
return render json: { error: "Unauthorized" }, status: :unauthorized unless valid_key.present?
@user = valid_key.user
render json: { error: "Unauthorized" }, status: :unauthorized unless @user
end
def heartbeat_keys
[
:branch,
:category,
:created_at,
:cursorpos,
:dependencies,
:editor,
:entity,
:is_write,
:language,
:line_additions,
:line_deletions,
:lineno,
:lines,
:machine,
:operating_system,
:project,
:project_root_count,
:time,
:type,
:user_agent
]
end
# allow either heartbeat or heartbeats
def heartbeat_bulk_params
params.require(:hackatime).permit(
heartbeats: [
*heartbeat_keys
]
)
end
def heartbeat_params
# Handle both direct params and _json format from WakaTime
if params[:_json].present?
params[:_json].first.permit(*heartbeat_keys)
elsif params[:hackatime].present?
params.require(:hackatime).permit(*heartbeat_keys)
else
params.permit(*heartbeat_keys)
end
end
end

View File

@@ -1,46 +0,0 @@
class HackatimeController < ApplicationController
before_action :set_user
def push_heartbeats
@user.heartbeats.create(heartbeat_params)
end
def push_heartbeats_bulk
@user.heartbeats.create(heartbeat_params)
end
private
def set_user
# each user has a Hackatime::User with an api_key
# the api_key is sent in the Authorization header as a Bearer token
api_key = request.headers["Authorization"].split(" ")[1]
@user = Hackatime::User.find_by(api_key: api_key)
render json: { error: "Unauthorized" }, status: :unauthorized unless @user
end
def heartbeat_params
params.require(:heartbeat).permit(
:branch,
:category,
:created_at,
:cursorpos,
:dependencies,
:editor,
:entity,
:is_write,
:language,
:line_additions,
:line_deletions,
:lineno,
:lines,
:machine,
:operating_system,
:project,
:project_root_count,
:time,
:type,
:user_agent
)
end
end

View File

@@ -21,6 +21,9 @@ module ApplicationHelper
end
def short_time_detailed(time)
# ie. 5h 10m 10s
# ie. 10m 10s
# ie. 10m
hours = time.to_i / 3600
minutes = (time.to_i % 3600) / 60
seconds = time.to_i % 60
@@ -31,4 +34,24 @@ module ApplicationHelper
time_parts << "#{seconds}s" if seconds.positive?
time_parts.join(" ")
end
def time_in_emoji(duration)
# ie. 15.hours => "🕒"
half_hours = duration.to_i / 1800
clocks = [
"🕛", "🕧",
"🕐", "🕜",
"🕑", "🕝",
"🕒", "🕞",
"🕓", "🕟",
"🕔", "🕠",
"🕕", "🕡",
"🕖", "🕢",
"🕗", "🕣",
"🕘", "🕤",
"🕙", "🕥",
"🕚", "🕦"
]
clocks[half_hours % clocks.length]
end
end

View File

@@ -1,5 +1,9 @@
class Heartbeat < ApplicationRecord
# This is to prevent Rails from trying to use STI even though we have a "type" column
self.inheritance_column = nil
belongs_to :user
validates :time, presence: true
validates :time, uniqueness: { scope: [ :user_id, :project, :branch, :language, :dependencies, :lineno, :cursorpos, :is_write, :entity, :type, :category, :project_root_count ] }
end

View File

@@ -18,6 +18,24 @@ class User < ApplicationRecord
has_many :api_keys
enum :hackatime_extension_text_type, {
simple_text: 0,
clock_emoji: 1,
compliment_text: 2
}
def format_extension_text(duration)
case hackatime_extension_text_type
when :simple_text
return "Start coding to track your time" if duration.zero?
::ApplicationController.helpers.short_time_simple(duration)
when :clock_emoji
::ApplicationController.helpers.time_in_emoji(duration)
when :compliment_text
"You're doing great!"
end
end
def admin?
is_admin
end

View File

@@ -48,15 +48,17 @@ Rails.application.routes.draw do
get "my/settings", to: "users#edit", as: :my_settings
patch "my/settings", to: "users#update"
post "/sailors_log/slack/commands", to: "slack#create"
post "/timedump/slack/commands", to: "slack#create"
# API routes
namespace :api do
namespace :wakatime do
namespace :hackatime do
namespace :v1 do
post "/heartbeats", to: "hackatime#push_heartbeats"
post "/heartbeats/bulk", to: "hackatime#push_heartbeats_bulk"
get "/", to: "hackatime#index"
get "/users/:id/statusbar/today", to: "hackatime#status_bar_today"
post "/users/:id/heartbeats", to: "hackatime#push_heartbeats"
post "/users/:id/heartbeats.bulk", to: "hackatime#push_heartbeats_bulk"
end
end
end

View File

@@ -22,7 +22,7 @@ class CreateHeartbeats < ActiveRecord::Migration[8.0]
t.integer :cursorpos
t.integer :project_root_count
t.datetime :time
t.float :time, null: false
t.boolean :is_write

View File

@@ -0,0 +1,5 @@
class AddExtensionTextTypeToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :hackatime_extension_text_type, :integer, default: 0, null: false
end
end

5
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_03_03_180842) do
ActiveRecord::Schema[8.0].define(version: 2025_03_04_032720) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -134,7 +134,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_03_180842) do
t.integer "lines"
t.integer "cursorpos"
t.integer "project_root_count"
t.datetime "time"
t.float "time", null: false
t.boolean "is_write"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -250,6 +250,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_03_180842) do
t.boolean "uses_slack_status", default: false, null: false
t.string "slack_scopes", default: [], array: true
t.text "slack_access_token"
t.integer "hackatime_extension_text_type", default: 0, null: false
t.index ["slack_uid"], name: "index_users_on_slack_uid", unique: true
end