Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 13 additions & 115 deletions app/controllers/api/admin/v1/admin_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,129 +28,27 @@ def visualization_quantized
user = find_user_by_id
return unless user

year = params[:year]&.to_i
month = params[:month]&.to_i
year = params[:year]
month = params[:month]

if year.nil? || month.nil? || month < 1 || month > 12
if year.blank? || month.blank?
render json: { error: "invalid parameters" }, status: :unprocessable_entity
Comment thread
skyfallwastaken marked this conversation as resolved.
return
end

begin
start_epoch = Time.utc(year, month, 1).to_i
end_epoch = if month == 12
Time.utc(year + 1, 1, 1).to_i
else
Time.utc(year, month + 1, 1).to_i
end
rescue Date::Error
render json: { error: "invalid date" }, status: :unprocessable_entity
return
end
result = ::Admin::VisualizationQuantizedQuery.new(
user: user,
year: year,
month: month
).call

quantized_query = <<-SQL
WITH base_heartbeats AS (
SELECT
"time",
lineno,
cursorpos,
date_trunc('day', to_timestamp("time")) as day_start
FROM heartbeats
WHERE user_id = ?
AND "time" >= ? AND "time" <= ?
LIMIT 1000000
),
daily_stats AS (
SELECT
*,
GREATEST(1, MAX(lineno) OVER (PARTITION BY day_start)) as max_lineno,
GREATEST(1, MAX(cursorpos) OVER (PARTITION BY day_start)) as max_cursorpos
FROM base_heartbeats
),
quantized_heartbeats AS (
SELECT
*,
ROUND(2 + (("time" - extract(epoch from day_start)) / 86400) * (396)) as qx,
ROUND(2 + (1 - CAST(lineno AS decimal) / max_lineno) * (96)) as qy_lineno,
ROUND(2 + (1 - CAST(cursorpos AS decimal) / max_cursorpos) * (96)) as qy_cursorpos
FROM daily_stats
)
SELECT "time", lineno, cursorpos
FROM (
SELECT DISTINCT ON (day_start, qx, qy_lineno) "time", lineno, cursorpos
FROM quantized_heartbeats
WHERE lineno IS NOT NULL
ORDER BY day_start, qx, qy_lineno, "time" ASC
) AS lineno_pixels
UNION
SELECT "time", lineno, cursorpos
FROM (
SELECT DISTINCT ON (day_start, qx, qy_cursorpos) "time", lineno, cursorpos
FROM quantized_heartbeats
WHERE cursorpos IS NOT NULL
ORDER BY day_start, qx, qy_cursorpos, "time" ASC
) AS cursorpos_pixels
UNION
SELECT "time", lineno, cursorpos
FROM (
SELECT DISTINCT ON (day_start, qx) "time", lineno, cursorpos
FROM quantized_heartbeats
WHERE lineno IS NULL AND cursorpos IS NULL
ORDER BY day_start, qx, "time" ASC
) AS null_pixels
ORDER BY "time" ASC
SQL

daily_totals_query = <<-SQL
WITH heartbeats_with_gaps AS (
SELECT
date_trunc('day', to_timestamp("time"))::date as day,
"time" - LAG("time", 1, "time") OVER (PARTITION BY date_trunc('day', to_timestamp("time")) ORDER BY "time") as gap
FROM heartbeats
WHERE user_id = ? AND time >= ? AND time <= ?
)
SELECT
day,
SUM(LEAST(gap, 120)) as total_seconds
FROM heartbeats_with_gaps
WHERE gap IS NOT NULL
GROUP BY day
SQL

quantized_result = ActiveRecord::Base.connection.execute(
ActiveRecord::Base.sanitize_sql([ quantized_query, user.id, start_epoch, end_epoch ])
)

daily_totals_result = ActiveRecord::Base.connection.execute(
ActiveRecord::Base.sanitize_sql([ daily_totals_query, user.id, start_epoch, end_epoch ])
)

daily_totals = daily_totals_result.each_with_object({}) do |row, hash|
day = row["day"]
total_seconds = row["total_seconds"]
hash[day] = total_seconds
end

points_by_day = quantized_result.each_with_object({}) do |row, hash|
day = Time.at(row["time"]).to_date
hash[day] ||= []
hash[day] << {
time: row["time"],
lineno: row["lineno"],
cursorpos: row["cursorpos"]
}
end

days = (start_epoch...end_epoch).step(86400).map do |epoch|
day = Time.at(epoch).to_date
{
date_timestamp_s: epoch,
total_seconds: daily_totals[day] || 0,
points: points_by_day[day] || []
}
unless result.success?
error = result.error == :invalid_date ? "invalid date" : "invalid parameters"
render json: { error: error }, status: :unprocessable_entity
return
end

render json: { days: days }
render json: { days: result.days }
end

def alt_candidates
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/api/v1/external_slack_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def create_user

user.slack_access_token = token

user_data = user.raw_slack_user_info
slack_service = Users::SlackIntegrationService.new(user)
user_data = slack_service.raw_slack_user_info
return render json: { error: "Invalid Slack token" }, status: :unauthorized unless user_data.present?

email = user_data.dig("profile", "email")
Expand All @@ -40,7 +41,7 @@ def create_user
email_address.source ||= :slack
user.email_addresses << email_address unless user.email_addresses.include?(email_address)

user.update_from_slack
slack_service.update_from_slack
user.parse_and_set_timezone(user_data["tz"])

if user.save
Expand Down
12 changes: 6 additions & 6 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ def hca_new
Rails.logger.info("Sessions return data: #{session[:return_data]}")
redirect_uri = url_for(action: :hca_create, only_path: false)

redirect_to User.hca_authorize_url(redirect_uri),
redirect_to Users::OauthAuthenticationService.hca_authorize_url(redirect_uri),
host: "https://auth.hackclub.com",
allow_other_host: "https://auth.hackclub.com"
end
Expand All @@ -23,7 +23,7 @@ def hca_create

redirect_uri = url_for(action: :hca_create, only_path: false)

@user = User.from_hca_token(params[:code], redirect_uri)
@user = Users::OauthAuthenticationService.from_hca_token(params[:code], redirect_uri)

if @user&.persisted?
session[:user_id] = @user.id
Expand Down Expand Up @@ -55,7 +55,7 @@ def slack_new
}.to_json

Rails.logger.info "Starting Slack OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.slack_authorize_url(redirect_uri, state: state_payload),
redirect_to Users::OauthAuthenticationService.slack_authorize_url(redirect_uri, state: state_payload),
host: "https://slack.com",
allow_other_host: "https://slack.com"
end
Expand All @@ -80,7 +80,7 @@ def slack_create
return
end

@user = User.from_slack_token(params[:code], redirect_uri)
@user = Users::OauthAuthenticationService.from_slack_token(params[:code], redirect_uri)

if @user&.persisted?
session[:user_id] = @user.id
Expand Down Expand Up @@ -118,7 +118,7 @@ def github_new
oauth_nonce = SecureRandom.hex(24)
session[:github_oauth_state_nonce] = oauth_nonce
Rails.logger.info "Starting GitHub OAuth flow with redirect URI: #{redirect_uri}"
redirect_to User.github_authorize_url(redirect_uri, state: oauth_nonce),
redirect_to Users::OauthAuthenticationService.github_authorize_url(redirect_uri, state: oauth_nonce),
allow_other_host: "https://github.com"
end

Expand All @@ -141,7 +141,7 @@ def github_create
return
end

@user = User.from_github_token(params[:code], redirect_uri, current_user)
@user = Users::OauthAuthenticationService.from_github_token(params[:code], redirect_uri, current_user)

if @user&.persisted?
PosthogService.capture(@user, "github_linked")
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/integrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def show

def update
if @user.update(integrations_params)
@user.update_slack_status if @user.uses_slack_status?
Users::SlackIntegrationService.new(@user).update_slack_status if @user.uses_slack_status?
PosthogService.capture(@user, "settings_updated", { fields: integrations_params.keys })
redirect_to my_settings_integrations_path, notice: "Settings updated successfully"
else
Expand Down
4 changes: 2 additions & 2 deletions app/jobs/one_time/backfill_email_sources_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ def perform
# Backfill email addresses from Slack

users.find_each do |user|
slack_user_info = user.raw_slack_user_info
github_user_info = user.raw_github_user_info
slack_user_info = Users::SlackIntegrationService.new(user).raw_slack_user_info
github_user_info = Users::GithubIntegrationService.new(user).raw_github_user_info

# sleep if we hit an api
sleep 1 unless slack_user_info.nil? && github_user_info.nil?
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/one_time/set_user_timezone_from_slack_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class OneTime::SetUserTimezoneFromSlackJob < ApplicationJob
def perform
User.where.not(slack_uid: nil).find_each(batch_size: 100) do |user|
begin
user.set_timezone_from_slack
Users::SlackIntegrationService.new(user).set_timezone_from_slack
user.save!
rescue => e
report_error(e, message: "Failed to update timezone for user #{user.id}")
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/slack_username_update_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def perform
.limit(100)
.each do |user|
begin
user.update_from_slack
Users::SlackIntegrationService.new(user).update_from_slack
user.save!
rescue => e
report_error(e, message: "Failed to update Slack username and avatar for user #{user.id}")
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/user_slack_status_update_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class UserSlackStatusUpdateJob < ApplicationJob
def perform
User.where(uses_slack_status: true).find_each(batch_size: BATCH_SIZE) do |user|
begin
user.update_slack_status
Users::SlackIntegrationService.new(user).update_slack_status
rescue => e
report_error(e, message: "Failed to update Slack status for user #{user.slack_uid}")
end
Expand Down
17 changes: 0 additions & 17 deletions app/models/concerns/github_integration.rb

This file was deleted.

92 changes: 92 additions & 0 deletions app/models/concerns/users/admin_and_trust.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
module Users
module AdminAndTrust
extend ActiveSupport::Concern

included do
enum :trust_level, {
blue: 0,
red: 1,
green: 2,
yellow: 3
}

enum :admin_level, {
default: 0,
superadmin: 1,
admin: 2,
viewer: 3,
ultraadmin: 4
}, prefix: :admin_level
end

class_methods do
def not_convicted
where.not(trust_level: trust_levels[:red])
end

def not_suspect
where(trust_level: [ trust_levels[:blue], trust_levels[:green] ])
end
end

def can_convict_users?
admin_level_superadmin? || admin_level_ultraadmin?
end

def set_admin_level(level)
return false unless level.present? && self.class.admin_levels.key?(level)

previous_level = admin_level

if previous_level != level.to_s
update!(admin_level: level.to_s)
end

true
end

def set_trust(level, changed_by_user: nil, reason: nil, notes: nil)
return false unless level.present?

previous_level = trust_level

if changed_by_user.present? && level.to_s == "red" && !changed_by_user.can_convict_users?
return false
end

if previous_level != level.to_s
if changed_by_user.present?
trust_level_audit_logs.create!(
changed_by: changed_by_user,
previous_trust_level: previous_level,
new_trust_level: level.to_s,
reason: reason,
notes: notes
)
end

update!(trust_level: level)
end

true
end

def active_deletion_request
deletion_requests.active.order(created_at: :desc).first
end

def pending_deletion?
active_deletion_request.present?
end

def can_request_deletion?
return false if pending_deletion?
return true unless red?

last_audit = trust_level_audit_logs.where(new_trust_level: :red).order(created_at: :desc).first
return true unless last_audit

last_audit.created_at <= 365.days.ago
end
end
end
Loading
Loading