mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 22:15:14 +00:00
Initial api & heartbeat rollover
This commit is contained in:
parent
954c5d5bde
commit
963f3303af
17 changed files with 399 additions and 1 deletions
16
app/avo/resources/api_key.rb
Normal file
16
app/avo/resources/api_key.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class Avo::Resources::ApiKey < Avo::BaseResource
|
||||
# self.includes = []
|
||||
# self.attachments = []
|
||||
# self.search = {
|
||||
# query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
|
||||
# }
|
||||
|
||||
def fields
|
||||
field :id, as: :id
|
||||
field :user, as: :text
|
||||
field :name, as: :textarea
|
||||
field :token, as: :textarea
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
29
app/avo/resources/heartbeat.rb
Normal file
29
app/avo/resources/heartbeat.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
class Avo::Resources::Heartbeat < Avo::BaseResource
|
||||
# self.includes = []
|
||||
# self.attachments = []
|
||||
# self.search = {
|
||||
# query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
|
||||
# }
|
||||
|
||||
def fields
|
||||
field :id, as: :id
|
||||
field :user, as: :text
|
||||
field :entity, as: :textarea
|
||||
field :type, as: :textarea
|
||||
field :category, as: :text
|
||||
field :time, as: :date_time
|
||||
field :project, as: :text
|
||||
field :project_root_count, as: :number
|
||||
field :branch, as: :text
|
||||
field :language, as: :text
|
||||
field :dependencies, as: :text
|
||||
field :lines, as: :number
|
||||
field :line_additions, as: :number
|
||||
field :line_deletions, as: :number
|
||||
field :lineno, as: :number
|
||||
field :cursorpos, as: :number
|
||||
field :is_write, as: :boolean
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
4
app/controllers/avo/api_keys_controller.rb
Normal file
4
app/controllers/avo/api_keys_controller.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# This controller has been generated to enable Rails' resource routes.
|
||||
# More information on https://docs.avohq.io/3.0/controllers.html
|
||||
class Avo::ApiKeysController < Avo::ResourcesController
|
||||
end
|
||||
4
app/controllers/avo/heartbeats_controller.rb
Normal file
4
app/controllers/avo/heartbeats_controller.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# This controller has been generated to enable Rails' resource routes.
|
||||
# More information on https://docs.avohq.io/3.0/controllers.html
|
||||
class Avo::HeartbeatsController < Avo::ResourcesController
|
||||
end
|
||||
46
app/controllers/hackatime_controller.rb
Normal file
46
app/controllers/hackatime_controller.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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)
|
||||
return 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
|
||||
58
app/jobs/one_time/migrate_user_from_hackatime_job.rb
Normal file
58
app/jobs/one_time/migrate_user_from_hackatime_job.rb
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
class OneTime::MigrateUserFromHackatimeJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id)
|
||||
@user = User.find(user_id)
|
||||
# Import from Hackatime
|
||||
import_api_keys
|
||||
import_heartbeats
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def import_heartbeats
|
||||
# create Heartbeat records for each Hackatime::Heartbeat in batches of 1000 as upsert
|
||||
|
||||
Hackatime::Heartbeat.where(user_id: @user.slack_uid).find_in_batches do |batch|
|
||||
Heartbeat.insert_all(
|
||||
batch.map { |heartbeat| {
|
||||
user_id: @user.id,
|
||||
time: heartbeat.time,
|
||||
project: heartbeat.project,
|
||||
branch: heartbeat.branch,
|
||||
category: heartbeat.category,
|
||||
dependencies: heartbeat.dependencies,
|
||||
editor: heartbeat.editor,
|
||||
entity: heartbeat.entity,
|
||||
language: heartbeat.language,
|
||||
machine: heartbeat.machine,
|
||||
operating_system: heartbeat.operating_system,
|
||||
type: heartbeat.type,
|
||||
user_agent: heartbeat.user_agent,
|
||||
line_additions: heartbeat.line_additions,
|
||||
line_deletions: heartbeat.line_deletions,
|
||||
lineno: heartbeat.line_number,
|
||||
lines: heartbeat.lines,
|
||||
cursorpos: heartbeat.cursor_position,
|
||||
project_root_count: heartbeat.project_root_count,
|
||||
is_write: heartbeat.is_write,
|
||||
} }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def import_api_keys
|
||||
puts "Importing API keys"
|
||||
hackatime_user = Hackatime::User.find(@user.slack_uid)
|
||||
return if hackatime_user.nil?
|
||||
|
||||
ApiKey.upsert(
|
||||
{
|
||||
user_id: @user.id,
|
||||
name: "Imported from Hackatime",
|
||||
token: hackatime_user.api_key,
|
||||
},
|
||||
unique_by: [:user_id, :token]
|
||||
)
|
||||
end
|
||||
end
|
||||
6
app/models/api_key.rb
Normal file
6
app/models/api_key.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class ApiKey < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates :name, presence: true, uniqueness: { scope: :user_id }
|
||||
end
|
||||
5
app/models/heartbeat.rb
Normal file
5
app/models/heartbeat.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Heartbeat < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :time, presence: true
|
||||
end
|
||||
|
|
@ -16,6 +16,8 @@ class User < ApplicationRecord
|
|||
primary_key: :slack_uid,
|
||||
class_name: "Hackatime::ProjectLabel"
|
||||
|
||||
has_many :api_keys
|
||||
|
||||
def admin?
|
||||
is_admin
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,4 +51,13 @@ Rails.application.routes.draw do
|
|||
|
||||
post "/sailors_log/slack/commands", to: "slack#create"
|
||||
post "/timedump/slack/commands", to: "slack#create"
|
||||
|
||||
namespace :api do
|
||||
namespace :wakatime do
|
||||
namespace :v1 do
|
||||
post "/heartbeats", to: "hackatime#push_heartbeats"
|
||||
post "/heartbeats/bulk", to: "hackatime#push_heartbeats_bulk"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
15
db/migrate/20250303175821_create_api_keys.rb
Normal file
15
db/migrate/20250303175821_create_api_keys.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class CreateApiKeys < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :api_keys do |t|
|
||||
t.belongs_to :user, null: false, foreign_key: true
|
||||
t.text :name, null: false
|
||||
t.text :token, null: false, index: { unique: true }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :api_keys, :token, unique: true
|
||||
add_index :api_keys, [:user_id, :token], unique: true
|
||||
add_index :api_keys, [:user_id, :name], unique: true
|
||||
end
|
||||
end
|
||||
55
db/migrate/20250303180842_create_heartbeats.rb
Normal file
55
db/migrate/20250303180842_create_heartbeats.rb
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
class CreateHeartbeats < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :heartbeats do |t|
|
||||
t.belongs_to :user, null: false, foreign_key: true
|
||||
|
||||
t.string :branch
|
||||
t.string :category
|
||||
t.string :dependencies, array: true, default: []
|
||||
t.string :editor
|
||||
t.string :entity
|
||||
t.string :language
|
||||
t.string :machine
|
||||
t.string :operating_system
|
||||
t.string :project
|
||||
t.string :type
|
||||
t.string :user_agent
|
||||
|
||||
t.integer :line_additions
|
||||
t.integer :line_deletions
|
||||
t.integer :lineno
|
||||
t.integer :lines
|
||||
t.integer :cursorpos
|
||||
t.integer :project_root_count
|
||||
|
||||
t.datetime :time
|
||||
|
||||
t.boolean :is_write
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :heartbeats, [
|
||||
:user_id,
|
||||
: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
|
||||
], unique: true
|
||||
end
|
||||
end
|
||||
89
db/schema.rb
generated
89
db/schema.rb
generated
|
|
@ -10,10 +10,22 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_03_03_180842) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
create_table "api_keys", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.text "name", null: false
|
||||
t.text "token", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["token"], name: "index_api_keys_on_token", unique: true
|
||||
t.index ["user_id", "name"], name: "index_api_keys_on_user_id_and_name", unique: true
|
||||
t.index ["user_id", "token"], name: "index_api_keys_on_user_id_and_token", unique: true
|
||||
t.index ["user_id"], name: "index_api_keys_on_user_id"
|
||||
end
|
||||
|
||||
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
|
@ -103,6 +115,33 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
|||
t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)"
|
||||
end
|
||||
|
||||
create_table "heartbeats", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "branch"
|
||||
t.string "category"
|
||||
t.string "dependencies", default: [], array: true
|
||||
t.string "editor"
|
||||
t.string "entity"
|
||||
t.string "language"
|
||||
t.string "machine"
|
||||
t.string "operating_system"
|
||||
t.string "project"
|
||||
t.string "type"
|
||||
t.string "user_agent"
|
||||
t.integer "line_additions"
|
||||
t.integer "line_deletions"
|
||||
t.integer "lineno"
|
||||
t.integer "lines"
|
||||
t.integer "cursorpos"
|
||||
t.integer "project_root_count"
|
||||
t.datetime "time"
|
||||
t.boolean "is_write"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id", "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"], name: "idx_on_user_id_branch_category_dependencies_editor__bfe8fefe9a", unique: true
|
||||
t.index ["user_id"], name: "index_heartbeats_on_user_id"
|
||||
end
|
||||
|
||||
create_table "leaderboard_entries", force: :cascade do |t|
|
||||
t.bigint "leaderboard_id", null: false
|
||||
t.string "user_id", null: false
|
||||
|
|
@ -122,6 +161,19 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
|||
t.datetime "deleted_at"
|
||||
end
|
||||
|
||||
create_table "project_checks", force: :cascade do |t|
|
||||
t.integer "check_type"
|
||||
t.integer "status"
|
||||
t.datetime "started_at"
|
||||
t.datetime "ended_at"
|
||||
t.text "output_message"
|
||||
t.jsonb "metadata"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["check_type", "created_at"], name: "index_project_checks_on_check_type_and_created_at"
|
||||
t.index ["status"], name: "index_project_checks_on_status"
|
||||
end
|
||||
|
||||
create_table "sailors_log_leaderboards", force: :cascade do |t|
|
||||
t.string "slack_channel_id"
|
||||
t.string "slack_uid"
|
||||
|
|
@ -157,6 +209,36 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "ship_chains", force: :cascade do |t|
|
||||
t.text "code_url"
|
||||
t.text "demo_url"
|
||||
t.text "readme_url"
|
||||
t.text "description"
|
||||
t.integer "ysws_type"
|
||||
t.bigint "user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["code_url"], name: "index_ship_chains_on_code_url"
|
||||
t.index ["demo_url"], name: "index_ship_chains_on_demo_url"
|
||||
t.index ["user_id"], name: "index_ship_chains_on_user_id"
|
||||
end
|
||||
|
||||
create_table "ship_update_descriptions", force: :cascade do |t|
|
||||
t.string "what_changed"
|
||||
t.bigint "ship_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["ship_id"], name: "index_ship_update_descriptions_on_ship_id"
|
||||
end
|
||||
|
||||
create_table "ships", force: :cascade do |t|
|
||||
t.bigint "ship_chain_id", null: false
|
||||
t.integer "duration"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["ship_chain_id"], name: "index_ships_on_ship_chain_id"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "slack_uid", null: false
|
||||
t.string "email", null: false
|
||||
|
|
@ -182,5 +264,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
|||
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
|
||||
end
|
||||
|
||||
add_foreign_key "api_keys", "users"
|
||||
add_foreign_key "heartbeats", "users"
|
||||
add_foreign_key "leaderboard_entries", "leaderboards"
|
||||
add_foreign_key "ship_chains", "users"
|
||||
add_foreign_key "ship_update_descriptions", "ships"
|
||||
add_foreign_key "ships", "ship_chains"
|
||||
end
|
||||
|
|
|
|||
11
test/fixtures/api_keys.yml
vendored
Normal file
11
test/fixtures/api_keys.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
user: one
|
||||
name: MyText
|
||||
token: MyText
|
||||
|
||||
two:
|
||||
user: two
|
||||
name: MyText
|
||||
token: MyText
|
||||
37
test/fixtures/heartbeats.yml
vendored
Normal file
37
test/fixtures/heartbeats.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
user: one
|
||||
entity: MyText
|
||||
type: MyText
|
||||
category: MyString
|
||||
time: 2025-03-03 18:08:42
|
||||
project: MyString
|
||||
project_root_count: 1
|
||||
branch: MyString
|
||||
language: MyString
|
||||
dependencies: MyString
|
||||
lines: 1
|
||||
line_additions: 1
|
||||
line_deletions: 1
|
||||
lineno: 1
|
||||
cursorpos: 1
|
||||
is_write: false
|
||||
|
||||
two:
|
||||
user: two
|
||||
entity: MyText
|
||||
type: MyText
|
||||
category: MyString
|
||||
time: 2025-03-03 18:08:42
|
||||
project: MyString
|
||||
project_root_count: 1
|
||||
branch: MyString
|
||||
language: MyString
|
||||
dependencies: MyString
|
||||
lines: 1
|
||||
line_additions: 1
|
||||
line_deletions: 1
|
||||
lineno: 1
|
||||
cursorpos: 1
|
||||
is_write: false
|
||||
7
test/models/api_key_test.rb
Normal file
7
test/models/api_key_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ApiKeyTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
7
test/models/heartbeat_test.rb
Normal file
7
test/models/heartbeat_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class HeartbeatTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue