use Time::HiRes;
use IO::Select;
use Unicode::Collate;
+use IO::Socket::SSL;
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';
use utf8;
require '../include/config.pm';
+my $global_ctx = IO::Socket::SSL::SSL_Context->new(
+ SSL_session_cache_size => 100, # Probably overkill.
+);
+IO::Socket::SSL::set_default_context($global_ctx);
+
my @log = ();
my $uca = Unicode::Collate->new(level => 1);
}
sub matches_name {
- my ($slack_name, $spreadsheet_name) = @_;
- if (sort_key($slack_name) eq sort_key($spreadsheet_name)) {
- return 1;
- }
+ my ($slack_name, $spreadsheet_name, $ap) = @_;
+
+ # No need to check for an exact match; we already did that through $seen_names.
+ # if (sort_key($slack_name) eq sort_key($spreadsheet_name)) {
+ # return 1;
+ # }
- my @ap = split /\s+/, $slack_name;
- my @bp = split /\s+/, $spreadsheet_name;
- if (scalar @ap >= 2 && scalar @bp >= 2 && sort_key($ap[0]) eq sort_key($bp[0])) {
+ # @ap is precalculated by the caller.
+ # my @ap = map { sort_key($_) } split /\s+/, $slack_name;
+ my @bp = map { sort_key($_) } split /\s+/, $spreadsheet_name;
+ if (scalar @$ap >= 2 && scalar @bp >= 2 && $ap->[0] eq $bp[0]) {
# First name matches, try to match some surname
my $found = 0;
- for my $ai (1..$#ap) {
+ for my $ai (1..(scalar @$ap)) {
for my $bi (1..$#bp) {
- $found = 1 if (sort_key($ap[$ai]) eq sort_key($bp[$bi]));
+ $found = 1 if ($ap->[$ai] eq $bp[$bi]);
}
}
if ($found) {
my $json = shift;
my %assignments = ();
- my $rows = $json->{'sheets'}[0]{'data'}[0]{'rowData'};
+ my $rows = $json->{'data'}[0]{'rowData'};
my @curr_groups = ();
for my $row (@$rows) {
my $col = 0;
sub update_assignment_db {
my ($dbh, $channel, $ts, $assignments) = @_;
- local $dbh->{AutoCommit} = 0;
my %db_assignments = ();
my $q = $dbh->prepare('SELECT name,group_name FROM current_group_membership_history WHERE channel=? AND ts=?');
$q->execute($channel, $ts);
$q->execute($channel, $ts, $name, undef);
}
}
- $dbh->commit;
}
sub get_spreadsheet_with_title {
- my ($ua, $token, $wanted_sheet_title) = @_;
+ my ($dbh, $ua, $token, $invitation_ts, $wanted_sheet_title) = @_;
# See if we have any spreadsheets that match this title.
my $start = [Time::HiRes::gettimeofday];
my $sheet_id = $sheet->{'properties'}{'sheetId'};
if ($title =~ /\Q$wanted_sheet_title\E/) {
# skv_log("Synkroniserer ($config::invitation_channel, $invitation_ts) mot arket “$title” (fane-ID $sheet_id).");
+ $dbh->do('UPDATE message_sheet_link SET tab_name=?, tab_id=? WHERE channel=? AND ts=?',
+ undef, $title, $sheet_id, $config::invitation_channel, $invitation_ts);
return ($title, $sheet_id);
}
}
my $json = shift;
my %seen_names = ();
- my $rows = $json->{'sheets'}[0]{'data'}[0]{'rowData'};
+ my $rows = $json->{'data'}[0]{'rowData'};
my $rowno = 3;
for my $row (@$rows) {
my $colno = 0;
}
sub possibly_nag_user {
- my ($dbh, $ua, $userid) = @_;
+ my ($dbh, $ua, $userid, $invitation_ts, $group, $slack_userid_to_slack_name) = @_;
+
+ my $slack_name = $slack_userid_to_slack_name->{$userid};
# See if we've nagged this user before.
- my $q = $dbh->prepare('SELECT * FROM users_nagged WHERE userid=?');
- $q->execute($userid);
+ my $q = $dbh->prepare('SELECT * FROM users_nagged WHERE userid=? AND ts=?');
+ $q->execute($userid, $invitation_ts);
if (defined($q->fetchrow_hashref)) {
return;
}
- my $msg = "Hei! Du meldte deg akkurat på trening, men vi klarer ikke å finne deg i en gruppe i regnearket. For at det skal være enklere for trenerne, ønsker vi gjerne at du går inn på https://regneark.skvidar.run/ og skriver deg inn der med samme navn som du bruker på Slack. Om du er usikker på hvilken gruppe som passer for deg, ta gjerne kontakt med en trener. Velkommen på trening og til klubben!";
+ my $msg;
+ if (!defined($group)) {
+ $msg = "Hei! Du meldte deg akkurat på trening, men vi klarer ikke å finne deg i en gruppe i regnearket. For at det skal være enklere for trenerne, ønsker vi gjerne at du går inn på https://regneark.skvidar.run/ og skriver deg inn der med samme navn som du bruker på Slack. Om du er usikker på hvilken gruppe som passer for deg, ta gjerne kontakt med en trener. Velkommen på trening og til klubben!";
+ skv_log("Sender Slack-melding til $slack_name ($userid) for å spørre om gruppe.");
+ } elsif ($group eq '(flere grupper)') {
+ $msg = "Hei! Du meldte deg akkurat på trening, men du ser ut til å stå i flere forskjellige grupper i regnearket. For at det skal være enklere for trenerne, ønsker vi gjerne at du går inn på https://regneark.skvidar.run/ og retter der. Om du er usikker på hvilken gruppe som passer for deg, ta gjerne kontakt med en trener. Velkommen på trening!";
+ skv_log("Sender Slack-melding til $slack_name ($userid) for å spørre om gruppe.");
+ } else {
+ $msg = "Hei! Du er påmeldt gruppe *$group*. Om dette er feil, gå gjerne inn og endre på https://regneark.skvidar.run/. Vi gleder oss til å se deg på trening!";
+ skv_log("Sender Slack-melding om at $slack_name ($userid) er i gruppe $group.");
+ }
- skv_log("Sender melding til $userid for å spørre om gruppe.");
my $content = {
channel => $config::invitation_channel,
user => $userid,
die "Something went wrong: " . $response->decoded_content if (!defined($msg_json) || !$msg_json->{'ok'});
# Mark that we've sent the message, so it won't happen again.
- $dbh->do('INSERT INTO users_nagged (userid, last_nag) VALUES (?, CURRENT_TIMESTAMP)', undef, $userid);
+ $dbh->do('INSERT INTO users_nagged (userid, ts, last_nag) VALUES (?, ?, CURRENT_TIMESTAMP)', undef, $userid, $invitation_ts);
}
sub db_connect {
if (!defined($dbh)) {
return undef;
}
+ $dbh->{AutoCommit} = 0;
$dbh->do('LISTEN skvupdate') or return undef;
return $dbh;
}
sub run {
- my $dbh = shift;
+ my ($dbh, $ua) = @_;
my $total_start = [Time::HiRes::gettimeofday];
@log = ();
skv_log("Siste sync startet: " . POSIX::ctime(time));
- # Initialize the handles we need for communication.
- my $ua = LWP::UserAgent->new('SKVidarLang/1.0');
+ # For the logic on the “applied” table below.
+ $dbh->do('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
+
my $token = get_oauth_bearer_token($dbh, $ua);
# Find the newest message, what it is linked to, and what was the one before it (for group diffing).
my $invitation_ts = $linkref->{'ts'};
my $wanted_sheet_title = $linkref->{'sheet_title'};
die "Could not get newest sheet title" if (!defined($wanted_sheet_title));
-
- my ($tab_name, $tab_id) = get_spreadsheet_with_title($ua, $token, $wanted_sheet_title);
- if (!defined($tab_name)) {
- skv_log("Fant ikke noen fane med «$wanted_sheet_title» i navnet; kan ikke synkronisere.\n");
- sheet_batch_update($ua, $token, [ serialize_skv_log_to_sheet() ]);
- die;
- }
+ my $tab_name = $linkref->{'tab_name'};
+ my $tab_id = $linkref->{'tab_id'};
# Store away the second-newest ID.
my $prev_invitation_ts = $q->fetchrow_hashref->{'ts'};
+ if (!defined($tab_name) || !defined($tab_id)) {
+ ($tab_name, $tab_id) = get_spreadsheet_with_title($dbh, $ua, $token, $invitation_ts, $wanted_sheet_title);
+ if (!defined($tab_name)) {
+ skv_log("Fant ikke noen fane med «$wanted_sheet_title» i navnet; kan ikke synkronisere.\n");
+ sheet_batch_update($ua, $token, [ serialize_skv_log_to_sheet() ]);
+ die;
+ }
+ }
+
# Find everyone who are marked as attending on Slack (via reactions).
$q = $dbh->prepare('SELECT DISTINCT userid,reaction FROM current_reactions WHERE channel=? AND ts=? AND reaction IN (\'heart\', \'open_mouth\', \'blue_heart\')');
$q->execute($config::invitation_channel, $invitation_ts);
delete $colors{$userid};
}
- # Get the list of all people in the sheet (we're going to need them soon anyway).
+ # Get the list of all people in the sheet (we're going to need them soon).
+ # Also get the Slack mapping when we're doing an API request anyway.
my $start = [Time::HiRes::gettimeofday];
- my $response = $ua->get('https://sheets.googleapis.com/v4/spreadsheets/' . $config::sheet_id . '?key=' . $config::gsheets_api_key . '&ranges=' . $tab_name . '!A4:Z5000&fields=sheets/data/rowData/values/userEnteredValue',
+ my $response = $ua->get('https://sheets.googleapis.com/v4/spreadsheets/' . $config::sheet_id . '?key=' . $config::gsheets_api_key . '&ranges=' . $tab_name . '!A4:Z5000&ranges=Slack-mapping!A5:C5000&fields=sheets/data/rowData/values/userEnteredValue',
Authorization => 'Bearer ' . $token,
Accept_Encoding => HTTP::Message::decodable
);
- log_timing($start, "/spreadsheets/$tab_name");
+ log_timing($start, "/spreadsheets/");
- my $main_sheet_json = JSON::XS::decode_json($response->decoded_content);
+ my $sheets_json = JSON::XS::decode_json($response->decoded_content);
+ my $main_sheet_json = $sheets_json->{'sheets'}[0];
+ my $mapping_sheet_json = $sheets_json->{'sheets'}[1];
# Update the list of groups we've seen people in.
my %assignments = get_group_assignments($main_sheet_json);
update_assignment_db($dbh, $config::invitation_channel, $invitation_ts, \%assignments);
+ $start = [Time::HiRes::gettimeofday];
my %seen_names = find_where_each_name_is($main_sheet_json);
+ log_timing($start, "Making sort key reverse mapping");
# Find duplicates.
for my $name (sort keys %seen_names) {
my %slack_userid_to_slack_name = ();
my %slack_userid_to_row = ();
- $start = [Time::HiRes::gettimeofday];
- $response = $ua->get('https://sheets.googleapis.com/v4/spreadsheets/' . $config::sheet_id . '?key=' . $config::gsheets_api_key . '&ranges=Slack-mapping!A5:C5000&fields=sheets/data/rowData/values/userEnteredValue',
- Authorization => 'Bearer ' . $token,
- Accept_Encoding => HTTP::Message::decodable
- );
- log_timing($start, "/spreadsheets/Slack-mapping");
- my $mapping_sheet_json = JSON::XS::decode_json($response->decoded_content);
- my $mapping_sheet_rows = $mapping_sheet_json->{'sheets'}[0]{'data'}[0]{'rowData'};
+ my $mapping_sheet_rows = $mapping_sheet_json->{'data'}[0]{'rowData'};
my $cur_row = 5;
for my $row (@$mapping_sheet_rows) {
my $slack_id = $row->{'values'}[0]{'userEnteredValue'}{'stringValue'};
} else {
# Do a search through all the available names in the sheet to find an obvious(ish) match.
my @candidates = ();
- my $main_sheet_rows = $main_sheet_json->{'sheets'}[0]{'data'}[0]{'rowData'};
+ my $main_sheet_rows = $main_sheet_json->{'data'}[0]{'rowData'};
+ $start = [Time::HiRes::gettimeofday];
+ my @ap = map { sort_key($_) } split /\s+/, $slack_name; # Precalc for matches_name().
for my $row (@$main_sheet_rows) {
for my $val (@{$row->{'values'}}) {
my $name = get_spreadsheet_name($val);
- if (defined($name) && matches_name($slack_name, $name)) {
+ if (defined($name) && matches_name($slack_name, $name, \@ap)) {
push @candidates, $name;
}
}
}
+ log_timing($start, "Fuzzy-searching for Slack name “$slack_name”");
if ($#candidates == -1) {
skv_log("$slack_name ($userid) er påmeldt på Slack, men fant ikke et regneark-navn for dem.");
- possibly_nag_user($dbh, $ua, $userid);
+ possibly_nag_user($dbh, $ua, $userid, $invitation_ts, undef, \%slack_userid_to_slack_name);
} elsif ($#candidates == 0) {
my $name = $candidates[0];
$slack_userid_to_real_name{$userid} = $name;
skv_log("$name er påmeldt flere steder på Slack; vet ikke hvilken som skal brukes.");
}
+ # ...and possibly send welcome messages to remind them of groups.
+ for my $userid (@attending_userids) {
+ my $real_name = $slack_userid_to_real_name{$userid};
+ next if (!defined($real_name));
+ my $group = $assignments{$real_name};
+ next if (!defined($group));
+ possibly_nag_user($dbh, $ua, $userid, $invitation_ts, $group, \%slack_userid_to_slack_name);
+ }
+
# Find the list of names to mark yellow.
my %want_colors = ();
- my $main_sheet_rows = $main_sheet_json->{'sheets'}[0]{'data'}[0]{'rowData'};
+ my $main_sheet_rows = $main_sheet_json->{'data'}[0]{'rowData'};
for my $userid (@attending_userids) {
next if (!exists($slack_userid_to_real_name{$userid}));
my $slack_name = $slack_userid_to_slack_name{$userid};
# Find the list of names we already marked yellow.
my %have_colors = ();
- $dbh->{AutoCommit} = 0;
- $dbh->do('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
$q = $dbh->prepare('SELECT name,color FROM applied WHERE channel=? AND ts=?');
$q->execute($config::invitation_channel, $invitation_ts);
while (my $ref = $q->fetchrow_hashref) {
printf "Tok %.0f ms.\n", 1e3 * $elapsed;
}
+# Initialize the handles we need for communication.
my $dbh = db_connect() or die;
+my $ua = LWP::UserAgent->new(agent => 'SKVidarLang/1.0', keep_alive => 50);
if ($#ARGV >= 0 && $ARGV[0] eq '--daemon') {
# Start with a single, forced run.
- run($dbh);
+ run($dbh, $ua);
while (1) {
while (!defined($dbh)) {
}
if (scalar @ready > 0) {
eval {
- $dbh->{AutoCommit} = 1;
- run($dbh);
- $dbh->commit;
+ run($dbh, $ua);
};
if ($@) {
warn "Died with: $@";
}
}
}
+} elsif ($#ARGV >= 0 && $ARGV[0] eq '--benchmark') {
+ for my $i (0..9) {
+ run($dbh, $ua);
+ }
} else {
- run($dbh);
+ run($dbh, $ua);
}