proca 2.2.1 → 2.5.0

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.
@@ -8,8 +8,7 @@ export default class CampaignStatus extends Command {
8
8
  static aliases = ["campaign:close"];
9
9
 
10
10
  static examples = [
11
- "<%= config.bin %> <%= command.id %> -name <campaign>",
12
- "<%= config.bin %> <%= command.id %> -i <campaign_id>",
11
+ "<%= config.bin %> <%= command.id %> -name <campaign> --end=2025-01-02 --status=close",
13
12
  ];
14
13
 
15
14
  static isCloseCommand =
@@ -18,46 +17,72 @@ export default class CampaignStatus extends Command {
18
17
  this.id?.includes("close");
19
18
 
20
19
  static flags = {
20
+ ...this.flagify({ name: "campaign" }),
21
21
  status: Flags.string({
22
- ...this.flagify({ multiid: true }),
23
22
  description: "Status to set",
24
23
  required: true,
25
24
  default: this.isCloseCommand ? "close" : undefined,
26
25
  options: ["draft", "live", "closed", "ignored"],
27
26
  }),
27
+ start: Flags.string({
28
+ description: "start date of the campaign",
29
+ helpValue: "YYYY-MM-DD",
30
+ parse: async (input) => {
31
+ const date = new Date(input);
32
+ if (Number.isNaN(date.getTime())) {
33
+ throw new Error(`Invalid date: ${input}`);
34
+ }
35
+ return date;
36
+ },
37
+ }),
38
+ end: Flags.string({
39
+ description: "end date of the campaign",
40
+ helpValue: "YYYY-MM-DD",
41
+ parse: async (input) => {
42
+ const date = new Date(input);
43
+ if (Number.isNaN(date.getTime())) {
44
+ throw new Error(`Invalid date: ${input}`);
45
+ }
46
+ return date;
47
+ },
48
+ }),
28
49
  };
29
50
 
30
51
  updateStatus = async (props) => {
31
52
  const Query = gql`
32
53
  mutation (
33
- $id: Int,
54
+ $id: Int
34
55
  $name: String
35
- $status: String!
56
+ $input: CampaignInput!
36
57
  ) {
37
- updateCampaign (id:$id, input: { name: $name,status: $status }) {
58
+ updateCampaign (id:$id, name: $name, input: $input) {
38
59
  name
39
60
  org {name}
40
61
  status
62
+ start
63
+ end
41
64
  title
42
65
  }
43
- }
44
- `;
66
+ }`;
67
+ const input = {
68
+ status: props.status.toUpperCase(),
69
+ };
45
70
 
46
71
  const result = await mutation(Query, {
47
72
  // org: props.org,
48
73
  id: props.id,
49
74
  name: props.name,
50
- status: props.status.toUpperCase(),
75
+ input: input,
51
76
  });
52
77
 
53
- console.log("result", result);
54
78
  return result.updateCampaign;
55
79
  };
56
80
 
57
81
  async run() {
58
- const { args, flags } = await this.parse();
82
+ const { flags } = await this.parse();
59
83
 
60
84
  const data = await this.updateStatus(flags);
61
- return this.output(data);
85
+
86
+ return this.output(data, { single: true });
62
87
  }
63
88
  }
@@ -0,0 +1,43 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { error, stdout, ux } from "@oclif/core/ux";
3
+ import { formatDistanceToNowStrict } from "date-fns";
4
+ import Command from "#src/procaCommand.mjs";
5
+ import {
6
+ FragmentOrg,
7
+ FragmentStats,
8
+ FragmentSummary,
9
+ } from "#src/queries/campaign.mjs";
10
+ import { gql, mutation } from "#src/urql.mjs";
11
+
12
+ export default class Delete extends Command {
13
+ static flags = {
14
+ ...this.flagify({ name: "org", char: "o" }),
15
+ ref: Flags.string({
16
+ char: "c",
17
+ required: true,
18
+ description: "contact reference",
19
+ helpValue: "from contact get",
20
+ }),
21
+ };
22
+
23
+ delete = async (ref, org) => {
24
+ const DeleteDocument = gql`mutation delete($ref: String!, $org: String!) {
25
+ deleteContact (contactRef: $ref, orgName: $org)
26
+ }`;
27
+
28
+ const r = await mutation(DeleteDocument, {
29
+ ref,
30
+ org,
31
+ });
32
+ if (r.errors) {
33
+ throw new Error(r.errors[0].message || "can't delete");
34
+ }
35
+ return r;
36
+ };
37
+
38
+ async run() {
39
+ const { flags } = await this.parse();
40
+ const data = await this.delete(flags.ref, flags.name);
41
+ return this.output(data);
42
+ }
43
+ }
@@ -0,0 +1,137 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { error, stdout, ux } from "@oclif/core/ux";
3
+ import { formatDistanceToNowStrict } from "date-fns";
4
+ import Command from "#src/procaCommand.mjs";
5
+ import {
6
+ FragmentOrg,
7
+ FragmentStats,
8
+ FragmentSummary,
9
+ } from "#src/queries/campaign.mjs";
10
+ import { gql, query } from "#src/urql.mjs";
11
+
12
+ export default class Get extends Command {
13
+ static flags = {
14
+ ...this.flagify({ name: "org", char: "o" }),
15
+ email: Flags.string({
16
+ char: "e",
17
+ required: true,
18
+ description: "email of the supporter",
19
+ helpValue: "<supporter@example.org>",
20
+ }),
21
+ utm: Flags.boolean({
22
+ description: "display the utm tracking parameters",
23
+ default: true,
24
+ allowNo: true,
25
+ exclusive: ["simplify"],
26
+ }),
27
+ comment: Flags.boolean({
28
+ description: "display the comment",
29
+ default: true,
30
+ allowNo: true,
31
+ exclusive: ["simplify"],
32
+ }),
33
+ };
34
+
35
+ fetch = async (flags) => {
36
+ const Document = gql`
37
+ query (
38
+ $orgName: String!
39
+ $email: String!
40
+ ) {
41
+ contacts(
42
+ orgName: $orgName
43
+ email: $email
44
+ ) {
45
+ actionId
46
+ actionPage {
47
+ id
48
+ locale
49
+ name
50
+ }
51
+ actionType
52
+ campaign {
53
+ name
54
+ }
55
+ contact {
56
+ contactRef
57
+ payload
58
+ nonce
59
+ publicKey {
60
+ public
61
+ }
62
+ }
63
+ createdAt
64
+ customFields
65
+ privacy {
66
+ emailStatus
67
+ emailStatusChanged
68
+ givenAt
69
+ optIn
70
+ withConsent
71
+ }
72
+ tracking {
73
+ campaign
74
+ content
75
+ medium
76
+ source
77
+ }
78
+ }
79
+ }
80
+ `;
81
+ const result = await query(Document, {
82
+ orgName: flags.name,
83
+ email: flags.email,
84
+ });
85
+ return result.contacts.map((d) => {
86
+ d.customFields = JSON.parse(d.customFields);
87
+ if (!d.contact.publicKey) {
88
+ const ref = d.contact.contactRef;
89
+ d.contact = JSON.parse(d.contact.payload);
90
+ d.contact.contactRef = ref;
91
+ } else {
92
+ this.error(
93
+ `encrypted contact we need the private key for ${d.contact.publicKey.public}`,
94
+ );
95
+ }
96
+ return d;
97
+ });
98
+ // return result.exportActions;
99
+ };
100
+
101
+ simplify = (d) => {
102
+ const result = {
103
+ contactRef: d.contact.contactRef,
104
+ firstname: d.contact.firstName,
105
+ country: d.contact.country,
106
+ email: d.contact.email,
107
+ type: d.actionType,
108
+ date: formatDistanceToNowStrict(d.createdAt),
109
+ campaign: !this.flags.campaign && d.campaign.name,
110
+ widget_id: d.actionPage.id,
111
+ widget: d.actionPage.name,
112
+ // customFields
113
+ };
114
+ if (this.flags.comment && d.customFields?.comment)
115
+ result.comment = d.customFields.comment;
116
+ if (this.flags.utm && d.tracking) {
117
+ result.utm_medium =
118
+ d.tracking.medium === "unknown" ? undefined : d.tracking.medium;
119
+ result.utm_source =
120
+ d.tracking.source === "unknown" ? undefined : d.tracking.source;
121
+ result.utm_campaign =
122
+ d.tracking.campaign === "unknown" ? undefined : d.tracking.campaign;
123
+ if (d.tracking.content)
124
+ result.utm_content =
125
+ d.tracking.content === "unknown" ? undefined : d.tracking.content;
126
+ }
127
+ if (d.customFields?.emailProvider)
128
+ result.provider = d.customFields.emailProvider;
129
+ return result;
130
+ };
131
+
132
+ async run() {
133
+ const { flags } = await this.parse();
134
+ const data = await this.fetch(flags);
135
+ return this.output(data, { single: true });
136
+ }
137
+ }
@@ -12,18 +12,9 @@ import { gql, query } from "#src/urql.mjs";
12
12
  export default class List extends Command {
13
13
  actionTypes = new Set();
14
14
 
15
- static examples = ["<%= config.bin %> <%= command.id %> %pizza%"];
16
-
17
15
  static flags = {
18
16
  // flag with no value (-f, --force)
19
- ...super.globalFlags,
20
- org: Flags.string({
21
- char: "o",
22
- description: "campaigns of the organisation (coordinator or partner)",
23
- required: true,
24
- // exactlyOne: ["org", "title"],
25
- helpValue: "<organisation name>",
26
- }),
17
+ ...this.flagify({ name: "org", char: "o" }),
27
18
  campaign: Flags.string({
28
19
  char: "c",
29
20
  description: "name of the campaign, % for wildchar",
@@ -138,7 +129,7 @@ export default class List extends Command {
138
129
  limit: flags.limit,
139
130
  onlyDoubleOptIn: flags.doi,
140
131
  onlyOptIn: flags.optin,
141
- orgName: flags.org,
132
+ orgName: flags.name,
142
133
  start: flags.start,
143
134
  });
144
135
  return result.contacts.map((d) => {
@@ -18,13 +18,7 @@ export default class OrgGet extends Command {
18
18
 
19
19
  static flags = {
20
20
  // flag with no value (-f, --force)
21
- ...this.flagify({ multiid: false }),
22
- name: Flags.string({
23
- char: "n",
24
- charAliases: ["o"],
25
- description: "name of the org",
26
- helpValue: "<org name>",
27
- }),
21
+ ...this.flagify({ multiid: false, name: "org", char: "o" }),
28
22
  config: Flags.boolean({
29
23
  description: "display the config",
30
24
  default: false,
@@ -113,8 +107,10 @@ export default class OrgGet extends Command {
113
107
  simplify = (d) => {
114
108
  const result = {
115
109
  id: d.id,
116
- Name: d.name,
117
- Title: d.title,
110
+ name: d.name,
111
+ title: d.title,
112
+ url: d.config?.url,
113
+ logo: d.config?.logo,
118
114
  "can targets reply?": d.replyEnabled ? true : undefined,
119
115
  "confirm actions?": d.supporterConfirm
120
116
  ? d.supporterConfirmTemplate
@@ -142,9 +138,7 @@ export default class OrgGet extends Command {
142
138
  };
143
139
 
144
140
  async run() {
145
- console.log("starting");
146
141
  const { flags } = await this.parse();
147
- console.log("flags", this.Flags);
148
142
 
149
143
  const data = await this.fetch(flags);
150
144
  return this.output(data);
@@ -0,0 +1,116 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { error, stdout, ux } from "@oclif/core/ux";
3
+ import Command from "#src/procaCommand.mjs";
4
+ //import {FragmentSummary,} from "#src/queries/org.mjs";
5
+ import { gql, mutation } from "#src/urql.mjs";
6
+ import { getOrg } from "./get.mjs";
7
+
8
+ export default class OrgLogo extends Command {
9
+ static description = "add a logo to the org";
10
+
11
+ static examples = ["<%= config.bin %> <%= command.id %> <name of the ngo>"];
12
+
13
+ static args = this.multiid();
14
+
15
+ static flags = {
16
+ // flag with no value (-f, --force)
17
+ ...this.flagify({ multiid: false, name: "org", char: "o" }),
18
+ url: Flags.string({
19
+ description: "use that url (and update it)",
20
+ }),
21
+ update: Flags.boolean({
22
+ default: true,
23
+ allowNo: true,
24
+ description: "use that url (and update it)",
25
+ }),
26
+ };
27
+
28
+ toProxyUrl = (pic) => {
29
+ const url = new URL(pic);
30
+ if (!url.hostname.endsWith("gstatic.com")) return googleUrl;
31
+ const sub = url.hostname.split(".")[0];
32
+ const domain = new URL(url.searchParams.get("url")).hostname;
33
+ return `https://pic.proca.app/favicon/${sub}/${domain}`;
34
+ };
35
+
36
+ fetchLogoUrl = async (domain) => {
37
+ const res = await fetch(
38
+ `https://www.google.com/s2/favicons?sz=128&domain=${domain}`,
39
+ { redirect: "manual" }, // don't follow — we want the Location header
40
+ );
41
+ const location = res.headers.get("location");
42
+ if (!location) throw new Error(`No redirect for ${domain}`);
43
+ return this.toProxyUrl(location);
44
+ };
45
+ update = async ({ name, config }) => {
46
+ const input = {};
47
+
48
+ input.config = typeof config === "string" ? config : JSON.stringify(config);
49
+
50
+ const Document = gql`
51
+ mutation update(
52
+ $orgName: String!
53
+ $input: OrgInput!
54
+ ) {
55
+ updateOrg(
56
+ name: $orgName
57
+ input: $input
58
+ ) {
59
+ id, name, config
60
+ }
61
+ }
62
+ `;
63
+
64
+ try {
65
+ console.log({
66
+ orgName: name,
67
+ input,
68
+ });
69
+ const r = await mutation(Document, {
70
+ orgName: name,
71
+ input,
72
+ });
73
+ return { id: r.updateOrg };
74
+ } catch (e) {
75
+ const err = e.graphQLErrors?.[0];
76
+
77
+ if (err?.extensions?.code === "permission_denied") {
78
+ this.error(`permission denied to update org ${name}`);
79
+ }
80
+ throw new Error(err?.message ?? "failed to update org");
81
+ }
82
+ };
83
+
84
+ table = (r) => {
85
+ super.table(r, null, null);
86
+ if (this.flags.config) {
87
+ r.config.locales = undefined;
88
+ this.prettyJson(r.config);
89
+ }
90
+ };
91
+
92
+ async run() {
93
+ const { flags } = await this.parse();
94
+ let logo;
95
+ let org;
96
+ if (flags.url) {
97
+ logo = await this.fetchLogoUrl(flags.url);
98
+ }
99
+ try {
100
+ org = await getOrg(flags);
101
+ } catch (e) {
102
+ this.output({ error: "user not a member of org", org: flags.name, logo });
103
+ return;
104
+ }
105
+ if (!logo) logo = await this.fetchLogoUrl(org.config.url);
106
+ org.config.logo = logo;
107
+ if (flags.update) {
108
+ if (!org.config.url) {
109
+ org.config.url = flags.url;
110
+ }
111
+ const r = this.update({ name: flags.name, config: org.config });
112
+ return this.output(r);
113
+ }
114
+ return this.output({ logo, name: flags.name });
115
+ }
116
+ }
@@ -1,6 +1,6 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import Command from "#src/procaCommand.mjs";
3
- import { gql, mutation, query } from "#src/urql.mjs";
3
+ import { gql, mutation } from "#src/urql.mjs";
4
4
 
5
5
  const SERVICE_NAMES = [
6
6
  "MAILJET",
@@ -14,9 +14,19 @@ const SERVICE_NAMES = [
14
14
  "SMTP",
15
15
  ].map((d) => d.toLowerCase());
16
16
 
17
- export default class OrgEmail extends Command {
17
+ export default class ServiceAdd extends Command {
18
18
  static description =
19
- "Set service, usually email backend for an org, the specific meaning of each param is dependant on the service";
19
+ "Set service, usually email backend for an org. the specific meaning of each param is dependant on the service. \nIf a service from that type exists, it will replace it";
20
+
21
+ // examples to add to help
22
+ // <%= config.bin %> resolves to the executable name
23
+ // <%= command.id %> resolves to the command name
24
+ static examples = [
25
+ // Examples can be simple strings
26
+ "<%= config.bin %> <%= command.id %> -o example_org --type system",
27
+ '<%= config.bin %> <%= command.id %> -o example_org --host=tls://mail.example.org:587 --user=login --password "secret" --type smtp',
28
+ '<%= config.bin %> <%= command.id %> -o example_org --host=ssl://mail.example.org:465 --user=login --password "secret" --type smtp',
29
+ ];
20
30
 
21
31
  static flags = {
22
32
  ...super.globalFlags,
@@ -29,7 +39,6 @@ export default class OrgEmail extends Command {
29
39
  description: "type of the service",
30
40
  options: SERVICE_NAMES,
31
41
  required: true,
32
- default: "system",
33
42
  }),
34
43
  user: Flags.string({
35
44
  description: "credential of the account on the service",
@@ -37,7 +46,7 @@ export default class OrgEmail extends Command {
37
46
  password: Flags.string({
38
47
  description: "credential of the account on the service",
39
48
  }),
40
- host: Flags.string({
49
+ host: Flags.url({
41
50
  description: "server of the service",
42
51
  }),
43
52
  path: Flags.string({
@@ -8,22 +8,19 @@ export default class UserJoinOrg extends Command {
8
8
 
9
9
  static examples = ["<%= config.bin %> <%= command.id %>"];
10
10
 
11
+ static args = this.multiid();
12
+
11
13
  static flags = {
12
- ...super.globalFlags,
14
+ // flag with no value (-f, --force)
15
+ ...this.flagify({ multiid: false, name: "org", char: "o" }),
13
16
  role: Flags.string({
14
17
  description: "permission level in that org",
15
18
  default: "campaigner",
16
19
  options: ["owner", "campaigner", "coordinator", "translator"],
17
20
  }),
18
- org: Flags.string({
19
- char: "o",
20
- required: true,
21
- description: "name of the org",
22
- helpValue: "<org name>",
23
- }),
24
21
  user: Flags.string({
25
22
  char: "u",
26
- description: "email",
23
+ description: "email, default current user",
27
24
  helpValue: "<user email>",
28
25
  }),
29
26
  };
@@ -58,7 +55,7 @@ mutation ($org: String!, $user: String!, $role: String = "campaigner") {
58
55
  `;
59
56
  const result = await mutation(Document, {
60
57
  user: params.user,
61
- org: params.org,
58
+ org: params.name,
62
59
  role: params.role,
63
60
  });
64
61
  return result.addOrgUser;
@@ -69,10 +66,10 @@ mutation ($org: String!, $user: String!, $role: String = "campaigner") {
69
66
  };
70
67
 
71
68
  async run() {
72
- const { args, flags } = await this.parse();
69
+ const { flags } = await this.parse();
73
70
  let data = undefined;
74
71
  if (!flags.user) {
75
- data = await this.join(flags.org);
72
+ data = await this.join(flags.name);
76
73
  } else {
77
74
  data = await this.mutate(flags);
78
75
  }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Flags } from "@oclif/core";
4
+ import { update } from "#src/commands/widget/external/index.mjs";
5
+ import Command from "#src/procaCommand.mjs";
6
+
7
+ export default class CounterExternal extends Command {
8
+ static description =
9
+ "Pull all external counters and save it into a widget extra Supporter. symlink the widget json into config/counter";
10
+
11
+ static examples = ["<%= config.bin %> <%= command.id %>"];
12
+
13
+ static flags = {
14
+ "dry-run": Flags.boolean({
15
+ description: "just fetch, don't update",
16
+ }),
17
+ };
18
+
19
+ async monitored() {
20
+ const dir = path.join(this.config.procaConfig.folder, "/counter");
21
+ try {
22
+ await fs.access(dir);
23
+ } catch {
24
+ this.error(`create the folder ${dir}`);
25
+ }
26
+ const entries = await fs.readdir(dir);
27
+ const results = await Promise.all(
28
+ entries.map(async (entry) => {
29
+ const file = entry.split(".");
30
+ if (file.length !== 2) {
31
+ this.warn(`invalid file ${entry}`);
32
+ return;
33
+ }
34
+ if (file[1] !== "json") {
35
+ this.warn(`should be a json file ${entry}`);
36
+ return;
37
+ }
38
+ const content = JSON.parse(
39
+ await fs.readFile(path.join(dir, entry), "utf-8"),
40
+ );
41
+ return {
42
+ id: Number.parseInt(file[0]),
43
+ name: content.filename,
44
+ url: content.component.counter?.url,
45
+ path: content.component.counter?.path,
46
+ };
47
+ }),
48
+ );
49
+ return results.flat();
50
+ }
51
+
52
+ async run() {
53
+ const { flags } = await this.parse(CounterExternal);
54
+ const widgets = await this.monitored();
55
+ if (flags["dry-run"]) return this.output(widgets);
56
+ const updated = await Promise.all(
57
+ widgets.map(async (widget) => {
58
+ return await update(widget);
59
+ }),
60
+ );
61
+ return this.output(updated);
62
+ }
63
+ }