mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-20 00:35:22 +00:00
Track sailors log leaderboards in the db
This commit is contained in:
parent
6731eb4316
commit
d88a7ee4b0
9 changed files with 182 additions and 128 deletions
|
|
@ -1,4 +1,4 @@
|
|||
class SailorsLogController < ApplicationController
|
||||
class SlackController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
before_action :verify_slack_request
|
||||
|
||||
|
|
@ -8,24 +8,28 @@ class SailorsLogController < ApplicationController
|
|||
|
||||
# Handle slack commands
|
||||
def create
|
||||
# Acknowledge receipt
|
||||
render json: {
|
||||
response_type: "ephemeral",
|
||||
blocks: [
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "#{params[:command]} #{params[:text]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
case params[:text].downcase.strip
|
||||
when "on"
|
||||
puts "Turning on notifications for #{params[:user_id]} in #{params[:channel_id]}"
|
||||
SailorsLogSetNotificationPrefJob.perform_later(params[:user_id], params[:channel_id], true)
|
||||
render json: {
|
||||
response_type: "in_channel",
|
||||
text: "@#{params[:user_name]} ran `/sailorslog on` to turn on High Seas notifications in this channel. Every time they code an hour on a project, a short message celebrating will be posted to this channel. They will also show on `/sailorslog leaderboard`."
|
||||
}
|
||||
SailorsLogSetNotificationPrefJob.perform_later(params[:user_id], params[:channel_id], true, params[:response_url], params[:user_name])
|
||||
when "off"
|
||||
SailorsLogSetNotificationPrefJob.perform_later(params[:user_id], params[:channel_id], false)
|
||||
render json: {
|
||||
response_type: "ephemeral",
|
||||
text: ":white_check_mark: Coding notifications have been turned off in this channel."
|
||||
}
|
||||
SailorsLogSetNotificationPrefJob.perform_later(params[:user_id], params[:channel_id], false, params[:response_url], params[:user_name])
|
||||
when "leaderboard"
|
||||
# Acknowledge receipt
|
||||
head :ok
|
||||
|
||||
# Send loading message first
|
||||
response = HTTP.post(params[:response_url], json: {
|
||||
response_type: "ephemeral",
|
||||
|
|
@ -33,18 +37,17 @@ class SailorsLogController < ApplicationController
|
|||
trigger_id: params[:trigger_id]
|
||||
})
|
||||
|
||||
puts "Response: #{response.body}"
|
||||
|
||||
# Process in background
|
||||
SailorsLogLeaderboardJob.perform_later(
|
||||
params[:channel_id],
|
||||
params[:user_id],
|
||||
params[:response_url],
|
||||
)
|
||||
else
|
||||
render json: {
|
||||
HTTP.post(params[:response_url], json: {
|
||||
response_type: "ephemeral",
|
||||
text: "Available commands: `/sailorslog on`, `/sailorslog off`, `/sailorslog leaderboard`"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -2,31 +2,25 @@ class SailorsLogLeaderboardJob < ApplicationJob
|
|||
queue_as :default
|
||||
include ApplicationHelper
|
||||
|
||||
def perform(channel_id, response_url)
|
||||
# Generate leaderboard
|
||||
leaderboard = SailorsLog.generate_leaderboard(channel_id)
|
||||
message = "*:boat: Sailor's Log - Today*"
|
||||
medals = [ "first_place_medal", "second_place_medal", "third_place_medal" ]
|
||||
def perform(channel_id, slack_uid, response_url)
|
||||
puts "Performing leaderboard job for channel #{channel_id} and user #{slack_uid}"
|
||||
# either find a leaderboard for this channel from the last 1 minute or create a new one
|
||||
leaderboard = SailorsLogLeaderboard.where(slack_channel_id: channel_id, deleted_at: nil)
|
||||
.where("created_at > ?", 1.minute.ago)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
|
||||
leaderboard.each_with_index do |entry, index|
|
||||
medal = medals[index] || "white_small_square"
|
||||
message += "\n:#{medal}: `<@#{entry[:user_id]}>`: #{short_time_simple entry[:duration]} → "
|
||||
message += entry[:projects].map do |project|
|
||||
language = project[:language_emoji] ? "#{project[:language_emoji]} #{project[:language]}" : project[:language]
|
||||
|
||||
project_entry = []
|
||||
project_entry << "#{project[:name]}"
|
||||
project_entry << "[#{language}]" unless language.nil?
|
||||
project_entry << "#{short_time_simple project[:duration]}"
|
||||
project_entry.join(" ")
|
||||
end.join(" + ")
|
||||
end
|
||||
# Create new leaderboard if none found
|
||||
leaderboard ||= SailorsLogLeaderboard.create!(
|
||||
slack_channel_id: channel_id,
|
||||
slack_uid: slack_uid
|
||||
)
|
||||
|
||||
# Update with final message
|
||||
response = HTTP.post(response_url, json: {
|
||||
response_type: "in_channel",
|
||||
replace_original: true,
|
||||
text: message
|
||||
text: leaderboard.message
|
||||
})
|
||||
|
||||
puts "Response: #{response.body}"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
class SailorsLogSetNotificationPrefJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(slack_uid, slack_channel_id, enabled)
|
||||
def perform(slack_uid, slack_channel_id, enabled, response_url, user_name)
|
||||
# set preference for the user
|
||||
slnp = SailorsLogNotificationPreference.find_or_initialize_by(slack_uid: slack_uid, slack_channel_id: slack_channel_id)
|
||||
slnp.enabled = enabled
|
||||
slnp.save!
|
||||
|
||||
# invalidate the leaderboard cache
|
||||
SailorsLogLeaderboard.where(slack_channel_id: slack_channel_id, deleted_at: nil).update_all(deleted_at: Time.current)
|
||||
|
||||
if enabled
|
||||
HTTP.post(response_url, json: {
|
||||
response_type: "in_channel",
|
||||
text: "@#{user_name} ran `/sailorslog on` to turn on High Seas notifications in this channel. Every time they code an hour on a project, a short message celebrating will be posted to this channel. They will also show on `/sailorslog leaderboard`."
|
||||
})
|
||||
else
|
||||
HTTP.post(response_url, json: {
|
||||
response_type: "ephemeral",
|
||||
text: ":white_check_mark: Coding notifications have been turned off in this channel."
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,93 +14,6 @@ class SailorsLog < ApplicationRecord
|
|||
foreign_key: :slack_uid,
|
||||
primary_key: :slack_uid
|
||||
|
||||
def self.generate_leaderboard(channel)
|
||||
# Get all users with enabled preferences in the channel
|
||||
users_in_channel = SailorsLogNotificationPreference.where(enabled: true, slack_channel_id: channel)
|
||||
.distinct
|
||||
.pluck(:slack_uid)
|
||||
|
||||
# Get all durations for users in channel
|
||||
user_durations = Heartbeat.where(user_id: users_in_channel)
|
||||
.today
|
||||
.group(:user_id)
|
||||
.duration_seconds
|
||||
|
||||
# Sort and take top 10 users
|
||||
top_user_ids = user_durations.sort_by { |_, duration| -duration }.first(10).map(&:first)
|
||||
|
||||
# Now get detailed project info only for top 10 users
|
||||
top_user_ids.map do |user_id|
|
||||
user_heartbeats = Heartbeat.where(user_id: user_id).today
|
||||
|
||||
# Get most common language per project using ActiveRecord
|
||||
most_common_languages = user_heartbeats
|
||||
.group(:project, :language)
|
||||
.count
|
||||
.group_by { |k, _| k[0] } # Group by project
|
||||
.transform_values { |langs| langs.max_by { |_, count| count }&.first&.last } # Get most common language
|
||||
|
||||
# Get all project durations in one query
|
||||
project_durations = user_heartbeats
|
||||
.group(:project)
|
||||
.duration_seconds
|
||||
|
||||
projects = project_durations.map do |project, duration|
|
||||
print "project: #{project}, duration: #{duration}, language: #{most_common_languages[project]}"
|
||||
{
|
||||
name: project,
|
||||
duration: duration,
|
||||
language: most_common_languages[project],
|
||||
language_emoji: self.language_emoji(most_common_languages[project])
|
||||
}
|
||||
end
|
||||
|
||||
projects = projects.filter { |project| project[:duration] > 1.minute }.sort_by { |project| -project[:duration] }
|
||||
|
||||
{
|
||||
user_id: user_id,
|
||||
duration: user_durations[user_id],
|
||||
projects: projects
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.language_emoji(language)
|
||||
language = language.downcase
|
||||
case language
|
||||
when "ruby"
|
||||
":#{language}:"
|
||||
when "javascript"
|
||||
":js:"
|
||||
when "typescript"
|
||||
":ts:"
|
||||
when "html"
|
||||
":#{language}:"
|
||||
when "java"
|
||||
[ ":java:", ":java_duke:" ].sample
|
||||
when "unity"
|
||||
[ ":unity:", ":unity_new:" ].sample
|
||||
when "c++"
|
||||
":#{language}:"
|
||||
when "c"
|
||||
[ ":c:", ":c_1:" ].sample
|
||||
when "rust"
|
||||
[ ":ferris:", ":crab:", ":ferrisowo:" ].sample
|
||||
when "python"
|
||||
[ ":snake:", ":python:", ":pf:", ":tw_snake:" ].sample
|
||||
when "nix"
|
||||
[ ":nix:", ":parrot-nix:" ].sample
|
||||
when "go"
|
||||
[ ":golang:", ":gopher:", ":gothonk:" ].sample
|
||||
when "kotlin"
|
||||
":#{language}:"
|
||||
when "astro"
|
||||
":#{language}:"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_projects_summary
|
||||
|
|
|
|||
|
|
@ -1,2 +1,117 @@
|
|||
class SailorsLogLeaderboard < ApplicationRecord
|
||||
include ApplicationHelper # Add this to get access to short_time_simple
|
||||
validates :slack_channel_id, :slack_uid, presence: true
|
||||
after_create :generate_message
|
||||
|
||||
private
|
||||
|
||||
def generate_message
|
||||
stats = SailorsLogLeaderboard.generate_leaderboard_stats(slack_channel_id)
|
||||
message = "*:boat: Sailor's Log - Today*"
|
||||
medals = [ "first_place_medal", "second_place_medal", "third_place_medal" ]
|
||||
|
||||
stats.each_with_index do |entry, index|
|
||||
medal = medals[index] || "white_small_square"
|
||||
message += "\n:#{medal}: `<@#{entry[:user_id]}>`: #{short_time_simple entry[:duration]} → "
|
||||
message += entry[:projects].map do |project|
|
||||
language = project[:language_emoji] ? "#{project[:language_emoji]} #{project[:language]}" : project[:language]
|
||||
|
||||
project_entry = []
|
||||
project_entry << "#{project[:name]}"
|
||||
project_entry << "[#{language}]" unless language.nil?
|
||||
project_entry << "#{short_time_simple project[:duration]}"
|
||||
project_entry.join(" ")
|
||||
end.join(" + ")
|
||||
end
|
||||
|
||||
# Update the message attribute and save
|
||||
update_column(:message, message)
|
||||
end
|
||||
|
||||
def self.generate_leaderboard_stats(channel)
|
||||
# Get all users with enabled preferences in the channel
|
||||
users_in_channel = SailorsLogNotificationPreference.where(enabled: true, slack_channel_id: channel)
|
||||
.distinct
|
||||
.pluck(:slack_uid)
|
||||
|
||||
# Get all durations for users in channel
|
||||
user_durations = Heartbeat.where(user_id: users_in_channel)
|
||||
.today
|
||||
.group(:user_id)
|
||||
.duration_seconds
|
||||
|
||||
# Sort and take top 10 users
|
||||
top_user_ids = user_durations.sort_by { |_, duration| -duration }.first(10).map(&:first)
|
||||
|
||||
# Now get detailed project info only for top 10 users
|
||||
top_user_ids.map do |user_id|
|
||||
user_heartbeats = Heartbeat.where(user_id: user_id).today
|
||||
|
||||
# Get most common language per project using ActiveRecord
|
||||
most_common_languages = user_heartbeats
|
||||
.group(:project, :language)
|
||||
.count
|
||||
.group_by { |k, _| k[0] } # Group by project
|
||||
.transform_values { |langs| langs.max_by { |_, count| count }&.first&.last } # Get most common language
|
||||
|
||||
# Get all project durations in one query
|
||||
project_durations = user_heartbeats
|
||||
.group(:project)
|
||||
.duration_seconds
|
||||
|
||||
projects = project_durations.map do |project, duration|
|
||||
print "project: #{project}, duration: #{duration}, language: #{most_common_languages[project]}"
|
||||
{
|
||||
name: project,
|
||||
duration: duration,
|
||||
language: most_common_languages[project],
|
||||
language_emoji: self.language_emoji(most_common_languages[project])
|
||||
}
|
||||
end
|
||||
|
||||
projects = projects.filter { |project| project[:duration] > 1.minute }.sort_by { |project| -project[:duration] }
|
||||
|
||||
{
|
||||
user_id: user_id,
|
||||
duration: user_durations[user_id],
|
||||
projects: projects
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.language_emoji(language)
|
||||
language = language.downcase
|
||||
case language
|
||||
when "ruby"
|
||||
":#{language}:"
|
||||
when "javascript"
|
||||
":js:"
|
||||
when "typescript"
|
||||
":ts:"
|
||||
when "html"
|
||||
":#{language}:"
|
||||
when "java"
|
||||
[ ":java:", ":java_duke:" ].sample
|
||||
when "unity"
|
||||
[ ":unity:", ":unity_new:" ].sample
|
||||
when "c++"
|
||||
":#{language}:"
|
||||
when "c"
|
||||
[ ":c:", ":c_1:" ].sample
|
||||
when "rust"
|
||||
[ ":ferris:", ":crab:", ":ferrisowo:" ].sample
|
||||
when "python"
|
||||
[ ":snake:", ":python:", ":pf:", ":tw_snake:" ].sample
|
||||
when "nix"
|
||||
[ ":nix:", ":parrot-nix:" ].sample
|
||||
when "go"
|
||||
[ ":golang:", ":gopher:", ":gothonk:" ].sample
|
||||
when "kotlin"
|
||||
":#{language}:"
|
||||
when "astro"
|
||||
":#{language}:"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,5 +44,5 @@ Rails.application.routes.draw do
|
|||
get "my/settings", to: "users#edit", as: :my_settings
|
||||
patch "my/settings", to: "users#update"
|
||||
|
||||
post "/slack/commands", to: "sailors_log#create"
|
||||
post "/slack/commands", to: "slack#create"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
class CreateSailorsLogLeaderboards < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :sailors_log_leaderboards do |t|
|
||||
t.string :slack_channel_id
|
||||
t.string :slack_uid
|
||||
t.text :message
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
class AddDeletedAtToSailorsLogLeaderboardJob < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :sailors_log_leaderboards, :deleted_at, :datetime, default: nil
|
||||
end
|
||||
end
|
||||
6
db/schema.rb
generated
6
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_02_23_072034) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_02_23_085114) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
|
@ -123,8 +123,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_23_072034) do
|
|||
end
|
||||
|
||||
create_table "sailors_log_leaderboards", force: :cascade do |t|
|
||||
t.string "slack_channel_id"
|
||||
t.string "slack_uid"
|
||||
t.text "message"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "deleted_at"
|
||||
end
|
||||
|
||||
create_table "sailors_log_notification_preferences", force: :cascade do |t|
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue