Skip to content

Commit f8d9d86

Browse files
authored
Feat/scoring (#5)
* poc: scoring * use scoring in postOrUpdate * add tsc to build
1 parent f36afcd commit f8d9d86

File tree

11 files changed

+181
-43
lines changed

11 files changed

+181
-43
lines changed

.github/workflows/docker-image.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
- uses: actions/checkout@v3
2121
- uses: oven-sh/setup-bun@v1
2222
- run: bun install --frozen-lockfile
23+
- run: bun run tsc
2324
- run: bun run build
2425
- name: Log in to the Container registry
2526
uses: docker/login-action@v2

bun.lockb

907 Bytes
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"date-fns-tz": "^2.0.0",
1616
"dotenv": "^16.0.3",
1717
"pg": "^8.11.3",
18-
"remeda": "^1.18.1"
18+
"remeda": "^1.27.0"
1919
},
2020
"devDependencies": {
2121
"@types/node": "^20.2.3",

src/db/queries.ts

+14
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ export async function getTodaysLeets(channelId: string) {
6969
return queryResult.rows as UserLeetRow[];
7070
}
7171

72+
export async function getLeetForDay(channelId: string, day: Date) {
73+
const queryResult = await client.query(
74+
`
75+
SELECT *
76+
FROM public.leet_messages
77+
WHERE channel = $1
78+
AND date(ts_as_date) = $2::date;
79+
`,
80+
[channelId, day],
81+
);
82+
83+
return queryResult.rows as UserLeetRow[];
84+
}
85+
7286
export async function getAllLeets(channelId: string): Promise<UserLeetRow[]> {
7387
const queryResult = await client.query(
7488
`

src/handlers/daily-leet/block-builder.ts

+14-16
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as R from "remeda";
2-
import { rowToStatus } from "./row-utils";
32
import { toCategoryMarkdown } from "./message-formatters";
4-
import { getTodaysLeets } from "../../db/queries";
53
import { formatHoursWithSeconds } from "../../utils/date-utils";
4+
import { ScoredDay } from "../score-engine";
65

7-
export function leetsToBlocks(
8-
hour: number,
9-
minutes: number,
10-
seconds: number,
11-
todaysLeets: Awaited<ReturnType<typeof getTodaysLeets>>,
12-
) {
13-
if (todaysLeets.length === 0) {
6+
export function scoredDayToBlocks(scoredDay: ScoredDay) {
7+
const leetCount = R.pipe(
8+
scoredDay,
9+
R.toPairs,
10+
R.flatMap(([, messages]) => messages),
11+
).length;
12+
13+
if (leetCount === 0) {
1414
return [
1515
{
1616
type: "section",
@@ -53,10 +53,8 @@ export function leetsToBlocks(
5353
text: {
5454
type: "mrkdwn",
5555
text: R.pipe(
56-
todaysLeets,
57-
R.sortBy((row) => row.ts),
58-
R.groupBy(rowToStatus),
59-
R.toPairs,
56+
scoredDay,
57+
R.toPairs.strict,
6058
R.map(toCategoryMarkdown),
6159
R.join("\n"),
6260
),
@@ -67,9 +65,9 @@ export function leetsToBlocks(
6765
elements: [
6866
{
6967
type: "mrkdwn",
70-
text: `:leetoo: ${
71-
todaysLeets.length
72-
} leets i dag, sist oppdatert ${formatHoursWithSeconds(new Date())}`,
68+
text: `:leetoo: ${leetCount} leets i dag, sist oppdatert ${formatHoursWithSeconds(
69+
new Date(),
70+
)}`,
7371
},
7472
],
7573
},

src/handlers/daily-leet/message-formatters.ts

+22-18
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,52 @@ import {
66
} from "../../utils/date-utils";
77
import { formatDistanceStrict, set } from "date-fns";
88
import { nb } from "date-fns/locale";
9+
import { LeetStatus } from "./row-utils";
10+
import { ScoredMessage } from "../score-engine";
911

10-
function formatLeetos(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
12+
type StatusScoredMessageTuple = [key: LeetStatus, rows: ScoredMessage[]];
13+
14+
function formatLeetos(rows: ScoredMessage[]) {
1115
return rows
1216
.map((row) => {
13-
const { ms } = getTimeParts(slackTsToDate(row.ts));
14-
return `:letopet: <@${row.message.user}>: ${ms}ms`;
17+
const { ms } = getTimeParts(slackTsToDate(row.message.ts));
18+
return `:letopet: <@${row.message.message.user}>: ${ms}ms (+${row.points})`;
1519
})
1620
.join("\n");
1721
}
1822

19-
function formatLeets(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
23+
function formatLeets(rows: ScoredMessage[]) {
2024
return rows
2125
.map((row) => {
22-
const { seconds, ms } = getTimeParts(slackTsToDate(row.ts));
23-
return `:letojam: <@${row.message.user}>: ${seconds}s ${ms}ms`;
26+
const { seconds, ms } = getTimeParts(slackTsToDate(row.message.ts));
27+
return `:letojam: <@${row.message.message.user}>: ${seconds}s ${ms}ms (+${row.points})`;
2428
})
2529
.join("\n");
2630
}
2731

28-
function formatPrematureRows(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
32+
function formatPrematureRows(rows: ScoredMessage[]) {
2933
return rows
3034
.map((row) => {
31-
const { seconds, ms } = getTimeParts(slackTsToDate(row.ts));
35+
const { seconds, ms } = getTimeParts(slackTsToDate(row.message.ts));
3236
const negativeOffset = 1000 - ms;
33-
return `:letogun: <@${row.message.user}>: -${negativeOffset}ms for tidlig :hot_face:`;
37+
return `:letogun: <@${row.message.message.user}>: -${negativeOffset}ms for tidlig :hot_face: (${row.points})`;
3438
})
3539
.join("\n");
3640
}
3741

38-
function formatLateRows(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
42+
function formatLateRows(rows: ScoredMessage[]) {
3943
return rows
4044
.map((row) => {
41-
const date = slackTsToDate(row.ts);
45+
const date = slackTsToDate(row.message.ts);
4246
const { minutes, seconds } = getTimeParts(date);
4347
return `:letoint: <@${
44-
row.message.user
45-
}>: ${minutes}m ${seconds}s (${formatHours(date)})`;
48+
row.message.message.user
49+
}>: ${minutes}m ${seconds}s (${formatHours(date)}) (+${row.points})`;
4650
})
4751
.join("\n");
4852
}
4953

50-
function messageFormatters(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
54+
function messageFormatters(rows: ScoredMessage[]) {
5155
return rows
5256
.map((row) => {
5357
const leet = set(new Date(), {
@@ -57,19 +61,19 @@ function messageFormatters(rows: Awaited<ReturnType<typeof getTodaysLeets>>) {
5761
milliseconds: 0,
5862
});
5963

60-
const date = slackTsToDate(row.ts);
61-
return `:letoshake: <@${row.message.user}>: ${formatDistanceStrict(
64+
const date = slackTsToDate(row.message.ts);
65+
return `:letoshake: <@${row.message.message.user}>: ${formatDistanceStrict(
6266
leet,
6367
date,
6468
{
6569
locale: nb,
6670
},
67-
)} bom :roflrofl: (${formatHours(date)})`;
71+
)} bom :roflrofl: (${formatHours(date)}) (+${row.points})`;
6872
})
6973
.join("\n");
7074
}
7175

72-
export function toCategoryMarkdown([key, rows]) {
76+
export function toCategoryMarkdown([key, rows]: StatusScoredMessageTuple) {
7377
switch (key) {
7478
case "leetos":
7579
return `*Ekte leetos*:\n${formatLeetos(rows)}`;

src/handlers/daily-leet/row-utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { getTimeParts, slackTsToDate } from "../../utils/date-utils";
22
import { getTodaysLeets } from "../../db/queries";
33

4+
export type LeetStatus = "leetos" | "leet" | "premature" | "late" | "garbage";
5+
46
export function rowToStatus(
57
row: Awaited<ReturnType<typeof getTodaysLeets>>[number],
6-
) {
8+
): LeetStatus {
79
const timeParts = getTimeParts(slackTsToDate(row.ts));
810

911
if (timeParts.minutes === 37 && timeParts.seconds === 0) {

src/handlers/leet-handlers.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
insertNewBotMessage,
1010
} from "../db/queries";
1111

12-
import { leetsToBlocks } from "./daily-leet/block-builder";
12+
import { scoredDayToBlocks } from "./daily-leet/block-builder";
13+
import { scoreDay } from "./score-engine";
1314

1415
export function configureLeetHandlers(app: App) {
1516
app.message("1337", async ({ message, say, event, client }) => {
@@ -72,12 +73,10 @@ export function configureLeetHandlers(app: App) {
7273
}
7374

7475
export async function postOrUpdate(client: WebClient, channelId: string) {
75-
const { hour, minutes, seconds } = getTimeParts(new Date());
76-
7776
console.log("Posting or updating");
7877
const existingTS = await getCurrentBotMessageTS(channelId);
79-
const todaysLeets = await getTodaysLeets(channelId);
80-
const blocks = leetsToBlocks(hour, minutes, seconds, todaysLeets);
78+
const scoredDay = await scoreDay(new Date(), channelId);
79+
const blocks = scoredDayToBlocks(scoredDay);
8180

8281
if (existingTS) {
8382
console.log(`Found existing ts: ${existingTS}`);

src/handlers/score-engine/index.ts

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as R from "remeda";
2+
import { getLeetForDay } from "../../db/queries";
3+
import { rowToStatus, LeetStatus } from "../daily-leet/row-utils";
4+
import { UserLeetRow } from "../../db/types";
5+
import { slackTsToMs, slackTsToSeconds } from "../../utils/date-utils";
6+
7+
export type ScoredMessage = {
8+
points: number;
9+
message: UserLeetRow;
10+
};
11+
12+
export type ScoredDay = Record<LeetStatus, ScoredMessage[]>;
13+
14+
export async function scoreDay(
15+
day: Date,
16+
channelId: string,
17+
): Promise<ScoredDay> {
18+
const leetsForDay: UserLeetRow[] = await getLeetForDay(channelId, day);
19+
const result: Record<LeetStatus, ScoredMessage[]> = R.pipe(
20+
leetsForDay,
21+
R.sortBy((row) => row.ts),
22+
R.groupBy(rowToStatus),
23+
(it) =>
24+
R.merge(
25+
{
26+
premature: [],
27+
leetos: [],
28+
leet: [],
29+
late: [],
30+
garbage: [],
31+
} satisfies Record<LeetStatus, UserLeetRow[]>,
32+
it,
33+
),
34+
scoreLeetos,
35+
scorePrematures,
36+
scoreLeets,
37+
zeroTheRest,
38+
);
39+
40+
return result;
41+
}
42+
43+
function scoreLeetos(
44+
day: Record<LeetStatus, UserLeetRow[]>,
45+
): Record<LeetStatus, (UserLeetRow | ScoredMessage)[]> {
46+
return {
47+
...day,
48+
leetos: day.leetos.map((leet): ScoredMessage => {
49+
const firstBonus = leet.ts === day.leetos[0].ts ? 500 : 0;
50+
const points = firstBonus + 1000 + (1000 - slackTsToMs(leet.ts));
51+
52+
return {
53+
points,
54+
message: leet,
55+
};
56+
}),
57+
};
58+
}
59+
60+
function scorePrematures(
61+
day: Record<LeetStatus, UserLeetRow[]>,
62+
): Record<LeetStatus, (UserLeetRow | ScoredMessage)[]> {
63+
return {
64+
...day,
65+
premature: day.premature.map((it): ScoredMessage => {
66+
return {
67+
points: -5000,
68+
message: it,
69+
};
70+
}),
71+
};
72+
}
73+
74+
function scoreLeets(
75+
day: Record<LeetStatus, UserLeetRow[]>,
76+
): Record<LeetStatus, (UserLeetRow | ScoredMessage)[]> {
77+
return {
78+
...day,
79+
leet: day.leet.map((it): ScoredMessage => {
80+
const firstBonus = day.leetos.length === 0 ? 500 : 0;
81+
const within3SecondsPoints = slackTsToSeconds(it.ts) <= 3 ? 250 : 0;
82+
83+
return {
84+
points: firstBonus + within3SecondsPoints,
85+
message: it,
86+
};
87+
}),
88+
};
89+
}
90+
91+
function zeroTheRest(
92+
day: Record<LeetStatus, UserLeetRow[]>,
93+
): Record<LeetStatus, ScoredMessage[]> {
94+
const zeroMessage = (it: UserLeetRow): ScoredMessage => {
95+
return {
96+
points: 0,
97+
message: it,
98+
};
99+
};
100+
101+
return {
102+
...day,
103+
late: day.late.map(zeroMessage),
104+
garbage: day.garbage.map(zeroMessage),
105+
} as unknown as Record<LeetStatus, ScoredMessage[]>;
106+
}

src/utils/date-utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as R from "remeda";
12
import { format, utcToZonedTime } from "date-fns-tz";
23
import {
34
getDate,
@@ -31,6 +32,16 @@ export function getTimeParts(time: Date): {
3132
};
3233
}
3334

35+
export const slackTsToMs: (ts: string) => number = R.createPipe(
36+
slackTsToDate,
37+
getMilliseconds,
38+
);
39+
40+
export const slackTsToSeconds: (ts: string) => number = R.createPipe(
41+
slackTsToDate,
42+
getSeconds,
43+
);
44+
3445
export function formatHours(time: Date) {
3546
return format(time, "HH:mm", { timeZone: OSLO_TZ });
3647
}

tsconfig.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"compilerOptions": {
33
"target": "es2015",
44
"moduleResolution": "node",
5-
"outDir": "dist"
6-
}
5+
"outDir": "dist",
6+
"skipLibCheck": true
7+
},
8+
"include": ["src/**/*"],
9+
"exclude": ["node_modules"]
710
}

0 commit comments

Comments
 (0)