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.
- package/CHANGELOG.md +267 -0
- package/package.json +20 -2
- package/src/Authors.ts +48 -0
- package/src/CNRepos.ts +3 -0
- package/src/EmailTasks.ts +100 -0
- package/src/Totals.ts +15 -0
- package/src/addCommentAction.ts +73 -0
- package/src/analyzePullsStatus.ts +79 -20
- package/src/analyzeTotals.ts +2 -4
- package/src/createGithubForkForRepo.ts +4 -1
- package/src/followRuleSchema.ts +14 -12
- package/src/index.ts +4 -1
- package/src/sendEmailAction.ts +56 -0
- package/src/sendGmail.ts +104 -0
- package/src/showFollowRuleSet.ts +12 -0
- package/src/updateAuthors.ts +7 -0
- package/src/updateAuthorsForGithub.ts +54 -0
- package/src/updateAuthorsFromCNRepo.ts +30 -0
- package/src/updateComfyTotals.ts +9 -9
- package/src/updateFollowRuleSet.ts +33 -78
- package/src/updateOutdatedPullsTemplates.ts +4 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
73
|
-
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/analyzeTotals.ts
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
}
|
package/src/followRuleSchema.ts
CHANGED
|
@@ -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
|
-
|
|
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":
|
|
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
|
-
.
|
|
73
|
-
.
|
|
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
|
+
}
|
package/src/sendGmail.ts
ADDED
|
@@ -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,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
|
+
}
|
package/src/updateComfyTotals.ts
CHANGED
|
@@ -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(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 });
|