Skip to content

Commit f33dbed

Browse files
committed
feat: track links in a json column
1 parent c29e41c commit f33dbed

File tree

5 files changed

+75
-4
lines changed

5 files changed

+75
-4
lines changed

app/db.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface MessageStats {
1717
char_count: number;
1818
code_stats: Generated<string>;
1919
guild_id: string;
20+
link_stats: Generated<string>;
2021
message_id: string | null;
2122
react_count: Generated<number>;
2223
recipient_id: string | null;

app/discord/activityTracker.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,24 @@ async function getMessageStats(msg: Message | PartialMessage) {
8888

8989
const blocks = parseMarkdownBlocks(content);
9090

91-
const [textblocks, codeblocks] = partition(blocks, (b) => b.type === "text");
91+
// TODO: groupBy would be better here, but this was easier to keep typesafe
92+
const [textblocks, nontextblocks] = partition(
93+
blocks,
94+
(b) => b.type === "text",
95+
);
96+
const [links, codeblocks] = partition(
97+
nontextblocks,
98+
(b) => b.type === "link",
99+
);
100+
101+
const linkStats = links.map((link) => ({ url: link.url }));
92102

93-
const { wordCount, charCount } = textblocks.reduce(
103+
const { wordCount, charCount } = [...links, ...textblocks].reduce(
94104
(acc, block) => {
95-
const words = getWords(block.content).length;
96-
const chars = getChars(block.content).length;
105+
const content =
106+
block.type === "link" ? (block.label ?? "") : block.content;
107+
const words = getWords(content).length;
108+
const chars = getChars(content).length;
97109
return {
98110
wordCount: acc.wordCount + words,
99111
charCount: acc.charCount + chars,
@@ -128,6 +140,7 @@ async function getMessageStats(msg: Message | PartialMessage) {
128140
char_count: charCount,
129141
word_count: wordCount,
130142
code_stats: JSON.stringify(codeStats),
143+
link_stats: JSON.stringify(linkStats),
131144
react_count: msg.reactions.cache.size,
132145
sent_at: msg.createdTimestamp,
133146
};

app/helpers/messageParsing.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
import { parseMarkdownBlocks } from "./messageParsing";
22

33
describe("markdown parser", () => {
4+
test("matches bare link", () => {
5+
const message = `bold claim (source https://trustme.bro)`;
6+
const result = parseMarkdownBlocks(message);
7+
expect(result[1]).toEqual({ type: "link", url: "https://trustme.bro" });
8+
});
9+
10+
test("matches link with label", () => {
11+
const message = `check out this [link](<https://example.com>)`;
12+
const result = parseMarkdownBlocks(message);
13+
expect(result[1]).toEqual({
14+
type: "link",
15+
url: "https://example.com",
16+
label: "link",
17+
});
18+
});
19+
20+
test("matches many links", () => {
21+
const message = `bare link https://asdf.com
22+
(see also https://links.com)
23+
* [*bold*](<https://foo.xyz>)
24+
* [*bold*](<https://bar.xyz>)
25+
words and things [\`foo()\`](<https://links.xom>) asdfasdf`;
26+
const result = parseMarkdownBlocks(message);
27+
const links = result.filter((x) => x.type === "link").map((x) => x.url);
28+
expect(links).toEqual([
29+
"https://asdf.com",
30+
"https://links.com",
31+
"https://foo.xyz",
32+
"https://bar.xyz",
33+
"https://links.xom",
34+
]);
35+
});
36+
437
test("matches fenced code blocks and inline text", () => {
538
const message = `here is some text
639

app/helpers/messageParsing.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export type MarkdownBlock =
22
| { type: "text"; content: string }
3+
| { type: "link"; url: string; label?: string }
34
| { type: "fencedcode"; lang: undefined | string; code: string[] }
45
| { type: "inlinecode"; code: string };
56

@@ -11,6 +12,8 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
1112
const matchers = {
1213
fencedCode: /```[\s\S]+?\n^```/,
1314
inlineCode: /`.+?`/,
15+
mdlink: /\[([^\]]+)\]\(([^)]+)\)/,
16+
link: /https?:\/\/[^\s>)]+/,
1417
};
1518

1619
// replaceAll gives easy access to the position of the match
@@ -37,6 +40,13 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
3740
// match is inline code, return a string without backticks
3841
const code = match.slice(1, -1);
3942
blocks.push({ type: "inlinecode", code });
43+
} else if (match.startsWith("[") || match.startsWith("http")) {
44+
// match is a link
45+
const label = captured[0] || undefined;
46+
let url = captured[1] || match;
47+
// links in discord may have angle brackets around them to suppress previews
48+
if (url.startsWith("<") && url.endsWith(">")) url = url.slice(1, -1);
49+
blocks.push({ type: "link", url, label });
4050
} else {
4151
console.error("unknown match", match);
4252
throw new Error("Unexpected match in markdown parsing");
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Kysely } from "kysely";
2+
3+
const column = "link_stats";
4+
5+
export async function up(db: Kysely<any>) {
6+
return await db.schema
7+
.alterTable("message_stats")
8+
.addColumn(column, "text", (c) => c.notNull().defaultTo("[]"))
9+
.execute();
10+
}
11+
12+
export async function down(db: Kysely<any>) {
13+
return db.schema.alterTable("message_stats").dropColumn(column).execute();
14+
}

0 commit comments

Comments
 (0)