comfy-pr 0.2.20 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,15 @@
1
1
  import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
2
- import pMap from "p-map";
2
+ import type { ObjectId } from "mongodb";
3
3
  import prettyMs from "pretty-ms";
4
+ import { snoflow } from "snoflow";
4
5
  import type { z } from "zod";
5
6
  import type { Task } from "../packages/mongodb-pipeline-ts/Task";
7
+ import type { Author } from "./Authors";
6
8
  import { CNRepos, type CRPull } from "./CNRepos";
9
+ import type { EmailTask } from "./EmailTasks";
7
10
  import type { GithubIssueComment } from "./GithubIssueComments";
8
11
  import { db } from "./db";
12
+ import { yaml } from "./utils/yaml";
9
13
  import type { zPullStatus } from "./zod/zPullsStatus";
10
14
  // import { $pipeline } from "./db/$pipeline";
11
15
  // in case of dump production in local environment:
@@ -22,12 +26,19 @@ if (import.meta.main) {
22
26
  // await writeFile("src/zPullsStatus.ts", jsonToZod(await analyzePullsStatus({ limit: 1 }), "zPullsStatus", true));
23
27
 
24
28
  // analyzePullsStatusPipeline
25
-
26
- const base = await pMap(analyzePullsStatusPipeline().group({ _id: "$ownername" }).aggregate(), (e) => e);
29
+ await snoflow(analyzePullsStatusPipeline().aggregate())
30
+ .filter((e) => e.email)
31
+ .limit(2)
32
+ .map((e) => yaml.stringify({ e }))
33
+ .log()
34
+ .chunk()
35
+ .map((e) => e.length)
36
+ .toLog();
27
37
  }
28
38
 
29
39
  export type PullStatus = z.infer<typeof zPullStatus>;
30
40
  export type PullsStatus = PullStatus[];
41
+ export type PullStatusShown = Awaited<ReturnType<typeof analyzePullsStatus>>[number];
31
42
  export async function analyzePullsStatus({ skip = 0, limit = 0, pipeline = analyzePullsStatusPipeline() } = {}) {
32
43
  "use server";
33
44
  return await pipeline
@@ -47,18 +58,25 @@ export async function analyzePullsStatus({ skip = 0, limit = 0, pipeline = analy
47
58
  })
48
59
  .toArray();
49
60
  }
50
- export function analyzePullsStatusPipeline() {
61
+ export function baseCRPullStatusPipeline() {
51
62
  return (
52
63
  $pipeline(CNRepos)
64
+ // get latest pr comments time
53
65
  .set({ "crPulls.data.pull.latest_comment_at": { $max: { $max: "$crPulls.data.comments.data.updated_at" } } })
54
- .unwind("$crPulls.data")
66
+ // unwind
67
+ .stage({ $unwind: "$crPulls.data" })
55
68
  .match({ "crPulls.data.comments.data": { $exists: true } })
69
+ // repo infos
56
70
  .set({ "crPulls.data.pull.actived_at": { $toDate: "$info.data.updated_at" } })
57
71
  .set({ "crPulls.data.pull.repo": "$repository" })
72
+ .set({ "crPulls.data.pull.email": "$email" })
58
73
  .set({ "crPulls.data.pull.on_registry": "$on_registry" })
74
+ // pull info
59
75
  .set({ "crPulls.data.pull.type": "$crPulls.data.type" })
60
- .set({ "crPulls.data.pull.comments": "$crPulls.data.comments.data" })
61
76
  .set({ "crPulls.data.pull": "$crPulls.data.pull" })
77
+ .set({ "crPulls.data.pull.comments": "$crPulls.data.comments.data" })
78
+ .set({ "crPulls.data.pull.emailTask_id": "$crPulls.data.emailTask_id" })
79
+ // replace root as pull
62
80
  .replaceRoot({ newRoot: "$crPulls.data.pull" })
63
81
  .as<
64
82
  CRPull & {
@@ -66,17 +84,59 @@ export function analyzePullsStatusPipeline() {
66
84
  on_registry: Task<boolean>;
67
85
  type: string;
68
86
  comments: GithubIssueComment[];
87
+ emailTask_id?: ObjectId;
69
88
  }
70
89
  >()
90
+ );
91
+ }
92
+ export function analyzePullsStatusPipeline() {
93
+ return (
94
+ baseCRPullStatusPipeline()
95
+ // fetch author email from Authors collection
96
+ .set({ ownername: "$base.user.login" })
97
+ .lookup({
98
+ from: "Authors",
99
+ // let: { <var_1>: <expression>, …, <var_n>: <expression> },
100
+ localField: "ownername",
101
+ foreignField: "githubId",
102
+ as: "author",
103
+ pipeline: [{ $project: { email: 1 } }],
104
+ })
105
+ .unwind({ path: "$author", preserveNullAndEmptyArrays: true })
106
+ .with<{ author: Author }>()
107
+ .lookup({
108
+ from: "EmailTasks",
109
+ // let: { <var_1>: <expression>, …, <var_n>: <expression> },
110
+ localField: "emailTask_id",
111
+ foreignField: "_id",
112
+ as: "emailTask",
113
+ })
114
+ .unwind({ path: "$emailTask", preserveNullAndEmptyArrays: true })
115
+ .set({ emailState: "$emailTask.state" }) //could be undefined or "waiting" | "sending" | "sent" | "error";
116
+ .with<{ emailState: EmailTask["state"] }>()
117
+
118
+ // .set({ author: { $first: ["$author"] } })
119
+ .project({ authors: 0 })
120
+
121
+ .set({ lastwords: { $arrayElemAt: ["$comments", -1] } })
122
+ .set({ lastwords: { $ifNull: [{ $concat: ["$lastwords.user.login", ": ", "$lastwords.body"] }, ""] } })
123
+
124
+ // calculate update_at max( )
125
+ // date format convert
126
+ .set({ created_at: { $toDate: "$created_at" } })
127
+ .set({ updated_at: { $toDate: "$updated_at" } })
128
+ .set({ latest_comment_at: { $toDate: "$latest_comment_at" } })
129
+ .set({ updated_at: { $max: ["$latest_comment_at", "$updated_at"] } })
130
+
131
+ // // projecting
71
132
  .project({
72
- created_at: { $toDate: "$created_at" },
73
- updated_at: { $toDate: "$updated_at" },
133
+ created_at: 1,
134
+ updated_at: 1,
74
135
  repository: 1,
75
136
  on_registry: "$on_registry.data",
76
137
  on_registry_at: "$on_registry.mtime",
77
138
  state: { $toUpper: `$prState` },
78
139
  url: "$html_url",
79
- author_email: "$base.user.email",
80
140
  ownername: "$base.user.login",
81
141
  nickName: "$base.user.name",
82
142
  head: { $concat: ["$user.login", ":", "$type"] },
@@ -93,16 +153,16 @@ export function analyzePullsStatusPipeline() {
93
153
  chars: " ",
94
154
  },
95
155
  },
96
- lastwords: { $arrayElemAt: ["$comments", -1] },
97
- latest_comment_at: { $toDate: "$latest_comment_at" },
156
+ lastwords: 1,
98
157
  actived_at: 1,
158
+
159
+ instagramId: "$author.instagramId",
160
+ discordId: "$author.discordId",
161
+ twitterId: "$author.twitterId",
162
+ email: { $ifNull: ["$author.email", ""] },
163
+ emailState: 1,
99
164
  })
100
- // .project({ latest_comment_at: {$toDate: '$latest_comment_at'} })
101
- .set({ updated_at: { $max: ["$latest_comment_at", "$updated_at"] } })
102
- .project({ latest_comment_at: 0 })
103
- .set({ lastwords: { $concat: ["$lastwords.user.login", ": ", "$lastwords.body"] } })
104
- .set({ lastwords: { $ifNull: ["$lastwords", ""] } })
105
- // .set({ state: { $nin: ["CLOSED"] } })
165
+ .unset("latest_comment_at")
106
166
  .set({
107
167
  CLOSED: { $eq: ["$state", "CLOSED"] },
108
168
  MERGED: { $eq: ["$state", "MERGED"] },
@@ -113,7 +173,7 @@ export function analyzePullsStatusPipeline() {
113
173
  .unset(["CLOSED", "MERGED", "OPEN"])
114
174
  .as<{
115
175
  actived_at: Date;
116
- author_email: string;
176
+ email: string | "";
117
177
  comments: number;
118
178
  created_at: Date;
119
179
  head: string;
@@ -126,9 +186,8 @@ export function analyzePullsStatusPipeline() {
126
186
  repository: string;
127
187
  state: "OPEN" | "MERGED" | "CLOSED";
128
188
  updated_at: Date;
189
+ emailState: EmailTask["state"];
129
190
  url: string;
130
191
  }>()
131
- // .stage({ ...(!!skip && { $skip: skip }) })
132
- // .stage({ ...(!!limit && { $limit: limit }) })
133
192
  );
134
193
  }
@@ -15,7 +15,7 @@ if (import.meta.main) {
15
15
  });
16
16
  }
17
17
  /**
18
- * @warning this function is heavy
18
+ * @warning this function is heavy, TODO: split into small chunk
19
19
  */
20
20
  export async function analyzeTotals() {
21
21
  "use server";
@@ -111,9 +111,7 @@ export async function analyzeTotals() {
111
111
 
112
112
  // // Follow Rules
113
113
  // "Follow Up Rules": (async function () {
114
- // await pMap($pipeline(FollowRuleSets).aggregate(), async (ruleset) => {
115
-
116
- // });
114
+ // return TaskDataOrNull(await showFollowRuleSet({ name: "default" }));
117
115
  // })(),
118
116
  });
119
117
  return totals;
@@ -32,6 +32,9 @@ export async function createGithubForkForRepo(upstreamRepoUrl: string) {
32
32
  const forkDst = `${FORK_OWNER}/${forkRepoName}`;
33
33
  const forkUrl = `https://github.com/${forkDst}`;
34
34
  const forked = await createGithubFork(upstreamRepoUrl, forkUrl);
35
- if (forked.html_url !== forkUrl) DIE("forked url not expected");
35
+ if (forked.html_url !== forkUrl)
36
+ DIE(
37
+ "forked url not expected, it's likely you already forked this repo in your account before, and now trying to fork it again with different salt. To recovery you could delete that repo forked before by manual. (the repo forked before is listed in FORK OK: .....)",
38
+ );
36
39
  return forked;
37
40
  }
@@ -30,12 +30,22 @@ export const zAddCommentAction = z
30
30
  body: z.string(),
31
31
  })
32
32
  .strict();
33
+ export const zSendEmailAction = z
34
+ .object({
35
+ provider: z.enum(["google"]),
36
+ name: z.string(),
37
+ from: z.string(),
38
+ to: z.string(),
39
+ subject: z.string(),
40
+ body: z.string(),
41
+ })
42
+ .strict();
33
43
  const zFollowUpRule = z.object({
34
44
  name: z.string(),
35
45
  $match: z
36
46
  .object({
37
47
  actived_at: mDate,
38
- author_email: mString,
48
+ email: mString,
39
49
  comments: mNumber,
40
50
  created_at: mDate,
41
51
  comments_author: mString,
@@ -53,15 +63,7 @@ const zFollowUpRule = z.object({
53
63
  action: z
54
64
  .object({
55
65
  "add-comment": zAddCommentAction,
56
- "send-email": z
57
- .object({
58
- provider: z.string(),
59
- from: z.string(),
60
- to: z.string(),
61
- subject: z.string(),
62
- body: z.string(),
63
- })
64
- .strict(), // WARN: not implementd
66
+ "send-email": zSendEmailAction, // WARN: not implementd
65
67
  "update-issue": z
66
68
  .object({
67
69
  tags: mAny,
@@ -69,8 +71,8 @@ const zFollowUpRule = z.object({
69
71
  .strict(),
70
72
  // close: z.any(),
71
73
  })
72
- .partial()
73
- .strict(),
74
+ .strict()
75
+ .partial(),
74
76
  });
75
77
  // zPullsStatus
76
78
  export const zFollowUpRules = zFollowUpRule.array();
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
- #!/usr/bin/env bun
2
1
  import "dotenv/config";
3
2
  import { checkComfyActivated } from "./checkComfyActivated";
3
+ import { updateEmailTasks } from "./EmailTasks";
4
4
  import { initializeFollowRules } from "./initializeFollowRules";
5
+ import { updateAuthors } from "./updateAuthors";
5
6
  import { updateCNRepos } from "./updateCNRepos";
6
7
  import { runFollowRuleSet } from "./updateFollowRuleSet";
7
8
  import { updateSlackMessages } from "./updateSlackMessages";
@@ -13,6 +14,8 @@ if (import.meta.main) {
13
14
  updateSlackMessages(),
14
15
  checkComfyActivated(), // needed if make pr
15
16
  updateCNRepos(),
17
+ updateAuthors(),
18
+ updateEmailTasks(),
16
19
  ]);
17
20
  await initializeFollowRules();
18
21
  await tLog("runFollowRuleSet", runFollowRuleSet);
@@ -0,0 +1,56 @@
1
+ "use server";
2
+ import { $elemMatch } from "@/packages/mongodb-pipeline-ts/$elemMatch";
3
+ import { TaskDataOrNull, type Task } from "@/packages/mongodb-pipeline-ts/Task";
4
+ import DIE from "@snomiao/die";
5
+ import pMap from "p-map";
6
+ import { snoflow } from "snoflow";
7
+ import type { z } from "zod";
8
+ import type { PullStatusShown } from "./analyzePullsStatus";
9
+ import { CNRepos } from "./CNRepos";
10
+ import { $filaten } from "./db";
11
+ import { enqueueEmailTask } from "./EmailTasks";
12
+ import { zSendEmailAction } from "./followRuleSchema";
13
+ import { yaml } from "./utils/yaml";
14
+
15
+ export async function sendEmailAction({
16
+ matched,
17
+ action,
18
+ runAction,
19
+ rule,
20
+ }: {
21
+ matched: Task<PullStatusShown[]>;
22
+ action: z.infer<typeof zSendEmailAction>;
23
+ runAction: boolean;
24
+ rule: { name: string };
25
+ }) {
26
+ return await pMap(
27
+ TaskDataOrNull(matched) ?? DIE("NO-PAYLOAD-AVAILABLE"),
28
+ async (payload) => {
29
+ if (action.provider !== "google") DIE("Currently we only support gmail sender");
30
+ const loadedAction = await snoflow([action])
31
+ .map((e) => yaml.stringify(e))
32
+ .map((e) =>
33
+ // replace {{var}} s
34
+ e.replace(
35
+ /{{\$([_A-Za-z0-9]+)}}/,
36
+ (_, key: string) =>
37
+ (payload as any)[key] || DIE("Missing key: " + key + " in payload: " + JSON.stringify(payload)),
38
+ ),
39
+ )
40
+ .map((e) => yaml.parse(e))
41
+ .map((y) => zSendEmailAction.parse(y))
42
+ .map((a) => ({ ...a, action: "send-email" }))
43
+ .toLast();
44
+
45
+ if (runAction) {
46
+ const task = await enqueueEmailTask(loadedAction);
47
+ console.log(rule.name + " email enqueued :" + yaml.stringify(loadedAction));
48
+ await CNRepos.updateOne($filaten({ crPulls: { data: $elemMatch({ pull: { html_url: payload.url } }) } }), {
49
+ $set: { "crPulls.data.$.emailTask_id": task._id },
50
+ });
51
+ }
52
+ return loadedAction;
53
+ },
54
+ { concurrency: 1 },
55
+ );
56
+ }
@@ -0,0 +1,104 @@
1
+ import DIE from "@snomiao/die";
2
+ import type { Credentials, OAuth2Client } from "google-auth-library";
3
+ import { GoogleApis } from "googleapis";
4
+ import { createMimeMessage } from "mail-mime-builder";
5
+ import markdownIt from "markdown-it";
6
+ import {
7
+ GCloudOAuth2Credentials,
8
+ getGCloudOAuth2Client,
9
+ handleGCloudOAuth2Callback,
10
+ } from "./gcloud/GCloudOAuth2Credentials";
11
+
12
+ if (import.meta.main) {
13
+ // send test email
14
+ const name = "snomiao";
15
+ const from = "snomiao@gmail.com";
16
+ const to = "snomiao@gmail.com";
17
+ const subject = "Hello! snomiao";
18
+ const markdown = `
19
+ # hello from sno
20
+
21
+ You've <b>just</b> received an *email* from snomiao.
22
+
23
+ Thank you for receiving this email!!!!
24
+ `;
25
+
26
+ const html = markdownIt().render(markdown);
27
+ console.log(html);
28
+
29
+ const sent = await sendGmail({
30
+ name,
31
+ from,
32
+ to,
33
+ subject,
34
+ html,
35
+ auth: await getGCloudOAuth2Client({
36
+ email: from,
37
+ scope: ["https://www.googleapis.com/auth/gmail.compose"],
38
+ authorize: async (url) => {
39
+ // check saved credential in db
40
+ const getCred = async () =>
41
+ (
42
+ await GCloudOAuth2Credentials.findOne({
43
+ scopes: "https://www.googleapis.com/auth/gmail.compose",
44
+ email: from,
45
+ credentials: { $exists: true },
46
+ })
47
+ )?.credentials;
48
+ const cred = await getCred();
49
+ if (cred) return cred;
50
+
51
+ // otherwise wait for user approve
52
+ (await (await import("open")).default(url)).unref();
53
+ // setup one time server to receive ?code={{....}}
54
+ return await new Promise<Credentials>((r) => {
55
+ const server = Bun.serve({
56
+ fetch: async (req) => {
57
+ // wait for code, and then save to db
58
+ const res = await handleGCloudOAuth2Callback(req);
59
+ const cred = await getCred();
60
+ if (cred) r(cred);
61
+ server.stop();
62
+ return res;
63
+ },
64
+ });
65
+ });
66
+ },
67
+ }),
68
+ });
69
+ console.log(sent);
70
+ }
71
+
72
+ export async function sendGmail({
73
+ name,
74
+ from,
75
+ to,
76
+ subject,
77
+ text,
78
+ html,
79
+ auth,
80
+ }: {
81
+ name: string;
82
+ from: string;
83
+ to: string;
84
+ subject: string;
85
+ text?: string;
86
+ html?: string;
87
+ auth: OAuth2Client;
88
+ }) {
89
+ const msg = createMimeMessage();
90
+ msg.setSender({ name: name, addr: from });
91
+ msg.setRecipient(to);
92
+ msg.setSubject(subject);
93
+ text && msg.addMessage({ contentType: "text/plain", data: text });
94
+ html && msg.addMessage({ contentType: "text/html", data: html });
95
+ text || html || DIE("Missing msg body");
96
+
97
+ const result = await new GoogleApis().gmail("v1").users.messages.send({
98
+ auth,
99
+ userId: from,
100
+ requestBody: { raw: btoa(msg.asRaw()) },
101
+ });
102
+ const sent = result.data;
103
+ return sent;
104
+ }
@@ -0,0 +1,12 @@
1
+ "use server";
2
+ import DIE from "@snomiao/die";
3
+ import { FollowRuleSets } from "./FollowRules";
4
+ import { updateFollowRuleSet } from "./updateFollowRuleSet";
5
+
6
+ export async function showFollowRuleSet({ name = "default" } = {}) {
7
+ const ruleset = (await FollowRuleSets.findOne({ name })) ?? DIE("default ruleset not found");
8
+ return await updateFollowRuleSet({
9
+ name: ruleset.name,
10
+ yaml: ruleset.yamlWhenEnabled ?? DIE("Rule not enabled"),
11
+ });
12
+ }
@@ -0,0 +1,7 @@
1
+ import { updateAuthorsForGithub } from "./updateAuthorsForGithub";
2
+ import { updateAuthorsFromCNRepo } from "./updateAuthorsFromCNRepo";
3
+
4
+ export async function updateAuthors() {
5
+ await updateAuthorsFromCNRepo();
6
+ await updateAuthorsForGithub();
7
+ }
@@ -0,0 +1,54 @@
1
+ import { $OK, TaskDataOrNull, TaskError, TaskOK } from "@/packages/mongodb-pipeline-ts/Task";
2
+ import DIE from "@snomiao/die";
3
+ import console from "console";
4
+ import { peekYaml } from "peek-log";
5
+ import { snoflow } from "snoflow";
6
+ import { Authors, GithubUsers } from "./Authors";
7
+ import { $stale } from "./db";
8
+ import { gh } from "./gh";
9
+
10
+ if (import.meta.main) {
11
+ await updateAuthorsForGithub();
12
+ }
13
+ export async function updateAuthorsForGithub() {
14
+ await snoflow(Authors.find({ githubMtime: $stale("7d") }))
15
+ .map((e) => e.githubId)
16
+ .filter()
17
+ .pMap(2, async (username) => {
18
+ const cached = await GithubUsers.findOne({ username, mtime: $stale("1d"), ...$OK });
19
+ if (cached) return cached;
20
+ console.log("Fetching github user " + username);
21
+ const result = await gh.users
22
+ .getByUsername({ username })
23
+ .then((e) => e.data)
24
+ .then(TaskOK)
25
+ .catch(TaskError);
26
+ return (
27
+ (await GithubUsers.findOneAndUpdate(
28
+ { username },
29
+ { $set: result },
30
+ { returnDocument: "after", upsert: true },
31
+ )) ?? DIE(`fail to udpate gh users`)
32
+ );
33
+ })
34
+ .map((e) => TaskDataOrNull(e))
35
+ .filter()
36
+ .map(({ email, avatar_url, blog, updated_at, location, company, hireable, bio, login }) =>
37
+ Authors.findOneAndUpdate(
38
+ { githubId: login },
39
+ {
40
+ $set: { githubMtime: new Date(), ...(email && { email }), ...(null != hireable && { hireable }) },
41
+ $addToSet: {
42
+ ...(bio && { bios: bio }),
43
+ avatars: avatar_url,
44
+ ...(location && { locations: location }),
45
+ ...(blog && { blogs: blog }),
46
+ ...(company && { companies: company }),
47
+ },
48
+ },
49
+ { upsert: true, returnDocument: "after" },
50
+ ),
51
+ )
52
+ .peek(peekYaml)
53
+ .done();
54
+ }
@@ -0,0 +1,30 @@
1
+ import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
2
+ import { snoflow } from "snoflow";
3
+ import { Authors } from "./Authors";
4
+ import { CNRepos } from "./CNRepos";
5
+ import { $filaten } from "./db";
6
+
7
+ /** Update authors for gh users, collecting emails/username/hireable */
8
+ export async function updateAuthorsFromCNRepo() {
9
+ return await snoflow(
10
+ $pipeline(CNRepos)
11
+ .match($filaten({ info: { data: { owner: { login: { $exists: true } } } } }))
12
+ .group({
13
+ _id: "$info.data.owner.login",
14
+ // author: "$info.data.owner.login",
15
+ cm: { $sum: { $cond: [{ $eq: [{ $type: "$cm" }, "missing"] }, 0, 1] } },
16
+ cr: { $sum: { $cond: [{ $eq: [{ $type: "$cr" }, "missing"] }, 0, 1] } },
17
+
18
+ // TODO: get totals open/closed/merged
19
+ // pulls:{
20
+ // OPEN: {"$crPulls.data.pull.prState", "open"}
21
+ // }
22
+ All: { $sum: 1 },
23
+ })
24
+ .set({ githubId: "$_id" })
25
+ .project({ _id: 0 })
26
+ .aggregate(),
27
+ )
28
+ .map(({ githubId, ...$set }) => Authors.updateOne({ githubId }, { $set }, { upsert: true }))
29
+ .done();
30
+ }
@@ -11,7 +11,7 @@ if (import.meta.main) {
11
11
  }
12
12
 
13
13
  export async function updateComfyTotals({ notify = true, fresh = "30m" } = {}) {
14
- await Totals.createIndex({today: 1, "totals.mtime": 1, "totals.state": 1})
14
+ await Totals.createIndex({ today: 1, "totals.mtime": 1, "totals.state": 1 });
15
15
  const today = new Date().toISOString().split("T")[0];
16
16
  const cached = await Totals.findOne($filaten({ today, totals: { mtime: $fresh(fresh), ...$OK } }));
17
17
  if (cached?.totals?.state === "ok")
@@ -24,14 +24,14 @@ export async function updateComfyTotals({ notify = true, fresh = "30m" } = {}) {
24
24
 
25
25
  // notify if today is not already notify
26
26
  if (notify) {
27
- if(!await Totals.findOne($filaten({ today, totals: { mtime: $fresh('0.9d'), ...$OK } })))
28
- // ignore today
29
- await match(totals)
30
- .with($OK, async (totals) => {
31
- const msg = `Totals: \n${"```"}\n${YAML.stringify(totals)}\n${"```"}`;
32
- await notifySlack(msg, { unique: true });
33
- })
34
- .otherwise(() => null);
27
+ if (!(await Totals.findOne($filaten({ today, totals: { mtime: $fresh("1d"), ...$OK } }))))
28
+ // ignore today
29
+ await match(totals)
30
+ .with($OK, async (totals) => {
31
+ const msg = `Totals: \n${"```"}\n${YAML.stringify(totals)}\n${"```"}`;
32
+ await notifySlack(msg, { unique: true });
33
+ })
34
+ .otherwise(() => null);
35
35
  }
36
36
 
37
37
  const insertResult = await Totals.insertOne({ totals });