proca 2.2.0 → 2.3.1

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/README.md CHANGED
@@ -74,6 +74,8 @@ you should also use the local proca-api in your [widget generator](https://githu
74
74
  * [`proca config user`](#proca-config-user)
75
75
  * [`proca contact area count`](#proca-contact-area-count)
76
76
  * [`proca contact count`](#proca-contact-count)
77
+ * [`proca contact delete`](#proca-contact-delete)
78
+ * [`proca contact get`](#proca-contact-get)
77
79
  * [`proca contact list`](#proca-contact-list)
78
80
  * [`proca help [COMMAND]`](#proca-help-command)
79
81
  * [`proca org add`](#proca-org-add)
@@ -81,6 +83,7 @@ you should also use the local proca-api in your [widget generator](https://githu
81
83
  * [`proca org delete`](#proca-org-delete)
82
84
  * [`proca org email`](#proca-org-email)
83
85
  * [`proca org get`](#proca-org-get)
86
+ * [`proca org logo`](#proca-org-logo)
84
87
  * [`proca org user get`](#proca-org-user-get)
85
88
  * [`proca plugins`](#proca-plugins)
86
89
  * [`proca plugins add PLUGIN`](#proca-plugins-add-plugin)
@@ -107,6 +110,7 @@ you should also use the local proca-api in your [widget generator](https://githu
107
110
  * [`proca user whoami`](#proca-user-whoami)
108
111
  * [`proca widget add`](#proca-widget-add)
109
112
  * [`proca widget delete`](#proca-widget-delete)
113
+ * [`proca widget external`](#proca-widget-external)
110
114
  * [`proca widget get`](#proca-widget-get)
111
115
  * [`proca widget list`](#proca-widget-list)
112
116
  * [`proca widget rebuild`](#proca-widget-rebuild)
@@ -337,12 +341,15 @@ EXAMPLES
337
341
  ```
338
342
  USAGE
339
343
  $ proca campaign close [ID_NAME_DXID] --status draft|live|closed|ignored [--json | --csv
340
- | --markdown] [--env <value>] [--simplify]
344
+ | --markdown] [--env <value>] [--simplify] [-n <campaign>] [--start YYYY-MM-DD] [--end YYYY-MM-DD]
341
345
 
342
346
  FLAGS
343
- --env=<value> [default: default] allow to switch between configurations (server or users)
344
- --status=<option> (required) Status to set
345
- <options: draft|live|closed|ignored>
347
+ -n, --name=<campaign> name (technical short name, also called slug)
348
+ --end=YYYY-MM-DD end date of the campaign
349
+ --env=<value> [default: default] allow to switch between configurations (server or users)
350
+ --start=YYYY-MM-DD start date of the campaign
351
+ --status=<option> (required) Status to set
352
+ <options: draft|live|closed|ignored>
346
353
 
347
354
  OUTPUT FLAGS
348
355
  --csv Format output as csv
@@ -354,9 +361,7 @@ ALIASES
354
361
  $ proca campaign close
355
362
 
356
363
  EXAMPLES
357
- $ proca campaign close -name <campaign>
358
-
359
- $ proca campaign close -i <campaign_id>
364
+ $ proca campaign close -name <campaign> --end=2025-01-02 --status=close
360
365
  ```
361
366
 
362
367
  ## `proca campaign copy`
@@ -526,12 +531,15 @@ EXAMPLES
526
531
  ```
527
532
  USAGE
528
533
  $ proca campaign status [ID_NAME_DXID] --status draft|live|closed|ignored [--json | --csv
529
- | --markdown] [--env <value>] [--simplify]
534
+ | --markdown] [--env <value>] [--simplify] [-n <campaign>] [--start YYYY-MM-DD] [--end YYYY-MM-DD]
530
535
 
531
536
  FLAGS
532
- --env=<value> [default: default] allow to switch between configurations (server or users)
533
- --status=<option> (required) Status to set
534
- <options: draft|live|closed|ignored>
537
+ -n, --name=<campaign> name (technical short name, also called slug)
538
+ --end=YYYY-MM-DD end date of the campaign
539
+ --env=<value> [default: default] allow to switch between configurations (server or users)
540
+ --start=YYYY-MM-DD start date of the campaign
541
+ --status=<option> (required) Status to set
542
+ <options: draft|live|closed|ignored>
535
543
 
536
544
  OUTPUT FLAGS
537
545
  --csv Format output as csv
@@ -543,9 +551,7 @@ ALIASES
543
551
  $ proca campaign close
544
552
 
545
553
  EXAMPLES
546
- $ proca campaign status -name <campaign>
547
-
548
- $ proca campaign status -i <campaign_id>
554
+ $ proca campaign status -name <campaign> --end=2025-01-02 --status=close
549
555
  ```
550
556
 
551
557
  ## `proca campaign widget archive`
@@ -961,18 +967,58 @@ EXAMPLES
961
967
  $ proca contact count --name <name of the campaign>
962
968
  ```
963
969
 
970
+ ## `proca contact delete`
971
+
972
+ ```
973
+ USAGE
974
+ $ proca contact delete -c from contact get [--json | --csv | --markdown] [--env <value>]
975
+ [--simplify] [-n <org>]
976
+
977
+ FLAGS
978
+ -c, --ref=from contact get (required) contact reference
979
+ -n, --name=<org> name (technical short name, also called slug)
980
+ --env=<value> [default: default] allow to switch between configurations (server or users)
981
+
982
+ OUTPUT FLAGS
983
+ --csv Format output as csv
984
+ --json Format output as json
985
+ --markdown Format output as markdown table
986
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
987
+ ```
988
+
989
+ ## `proca contact get`
990
+
991
+ ```
992
+ USAGE
993
+ $ proca contact get -e <supporter@example.org> [--json | --csv | --markdown] [--env
994
+ <value>] [-n <org>] [--utm | --simplify] [--comment | ]
995
+
996
+ FLAGS
997
+ -e, --email=<supporter@example.org> (required) email of the supporter
998
+ -n, --name=<org> name (technical short name, also called slug)
999
+ --[no-]comment display the comment
1000
+ --env=<value> [default: default] allow to switch between configurations (server or users)
1001
+ --[no-]utm display the utm tracking parameters
1002
+
1003
+ OUTPUT FLAGS
1004
+ --csv Format output as csv
1005
+ --json Format output as json
1006
+ --markdown Format output as markdown table
1007
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
1008
+ ```
1009
+
964
1010
  ## `proca contact list`
965
1011
 
966
1012
  ```
967
1013
  USAGE
968
- $ proca contact list -o <organisation name> [--json | --csv | --markdown] [--env
969
- <value>] [-c <campaign title>] [-n <value>] [--today | --after 2025-04-09] [--optin] [--testing] [--doi] [--utm |
970
- --simplify] [--comment | ]
1014
+ $ proca contact list [--json | --csv | --markdown] [--env <value>] [-n <org>] [-c
1015
+ <campaign title>] [-n <value>] [--today | --after 2025-04-09] [--optin] [--testing] [--doi] [--utm | --simplify]
1016
+ [--comment | ]
971
1017
 
972
1018
  FLAGS
973
1019
  -c, --campaign=<campaign title> name of the campaign, % for wildchar
974
1020
  -n, --limit=<value> max number of actions
975
- -o, --org=<organisation name> (required) campaigns of the organisation (coordinator or partner)
1021
+ -n, --name=<org> name (technical short name, also called slug)
976
1022
  --after=2025-04-09 only actions after a date
977
1023
  --[no-]comment display the comment
978
1024
  --doi only export the double optin actions
@@ -987,9 +1033,6 @@ OUTPUT FLAGS
987
1033
  --json Format output as json
988
1034
  --markdown Format output as markdown table
989
1035
  --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
990
-
991
- EXAMPLES
992
- $ proca contact list %pizza%
993
1036
  ```
994
1037
 
995
1038
  ## `proca help [COMMAND]`
@@ -1126,10 +1169,10 @@ view a org
1126
1169
  ```
1127
1170
  USAGE
1128
1171
  $ proca org get [ID_NAME_DXID] [--json | --csv | --markdown] [--env <value>]
1129
- [--simplify] [-n <org name>] [--config] [--personaldata] [--processing] [--keys] [--campaigns] [--users]
1172
+ [--simplify] [-n <org>] [--config] [--personaldata] [--processing] [--keys] [--campaigns] [--users]
1130
1173
 
1131
1174
  FLAGS
1132
- -n, --name=<org name> name of the org
1175
+ -n, --name=<org> name (technical short name, also called slug)
1133
1176
  --[no-]campaigns
1134
1177
  --[no-]config display the config
1135
1178
  --env=<value> [default: default] allow to switch between configurations (server or users)
@@ -1151,6 +1194,36 @@ EXAMPLES
1151
1194
  $ proca org get <name of the ngo>
1152
1195
  ```
1153
1196
 
1197
+ ## `proca org logo`
1198
+
1199
+ add a logo to the org
1200
+
1201
+ ```
1202
+ USAGE
1203
+ $ proca org logo [ID_NAME_DXID] [--json | --csv | --markdown] [--env <value>]
1204
+ [--simplify] [-n <org>] [--url <value>] [--update]
1205
+
1206
+ FLAGS
1207
+ -n, --name=<org> name (technical short name, also called slug)
1208
+ --env=<value> [default: default] allow to switch between configurations (server or users)
1209
+ --[no-]update use that url (and update it)
1210
+ --url=<value> use that url (and update it)
1211
+
1212
+ OUTPUT FLAGS
1213
+ --csv Format output as csv
1214
+ --json Format output as json
1215
+ --markdown Format output as markdown table
1216
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
1217
+
1218
+ DESCRIPTION
1219
+ add a logo to the org
1220
+
1221
+ EXAMPLES
1222
+ $ proca org logo <name of the ngo>
1223
+ ```
1224
+
1225
+ _See code: [src/commands/org/logo.ts](https://github.com/fixthestatusquo/proca-cli/blob/v2.3.1/src/commands/org/logo.ts)_
1226
+
1154
1227
  ## `proca org user get`
1155
1228
 
1156
1229
  list all the users
@@ -1654,12 +1727,12 @@ let a user join an organisation with a role
1654
1727
 
1655
1728
  ```
1656
1729
  USAGE
1657
- $ proca user join -o <org name> [--json | --csv | --markdown] [--env <value>]
1658
- [--simplify] [--role owner|campaigner|coordinator|translator] [-u <user email>]
1730
+ $ proca user join [ID_NAME_DXID] [--json | --csv | --markdown] [--env <value>]
1731
+ [--simplify] [-n <org>] [--role owner|campaigner|coordinator|translator] [-u <user email>]
1659
1732
 
1660
1733
  FLAGS
1661
- -o, --org=<org name> (required) name of the org
1662
- -u, --user=<user email> email
1734
+ -n, --name=<org> name (technical short name, also called slug)
1735
+ -u, --user=<user email> email, default current user
1663
1736
  --env=<value> [default: default] allow to switch between configurations (server or users)
1664
1737
  --role=<option> [default: campaigner] permission level in that org
1665
1738
  <options: owner|campaigner|coordinator|translator>
@@ -1863,6 +1936,40 @@ DESCRIPTION
1863
1936
  Delete a widget
1864
1937
  ```
1865
1938
 
1939
+ ## `proca widget external`
1940
+
1941
+ Pull external counter and save it into a widget extra Supporter
1942
+
1943
+ ```
1944
+ USAGE
1945
+ $ proca widget external [ID_NAME_DXID] [--json | --csv | --markdown] [--env <value>]
1946
+ [--simplify] [-i <value> | -n <the_short_name> | -x <value>] [-u <value>] [--path object.sub-object.total] [--total
1947
+ <value>] [--timeout <value>] [--dry-run]
1948
+
1949
+ FLAGS
1950
+ -i, --id=<value>
1951
+ -n, --name=<the_short_name> name (technical short name, also called slug)
1952
+ -u, --url=<value> API endpoint URL to pull from
1953
+ -x, --dxid=<value> dxid
1954
+ --dry-run just fetch, don't update
1955
+ --env=<value> [default: default] allow to switch between configurations (server or users)
1956
+ --path=object.sub-object.total dot notation path to the counter field in the json returned by the url
1957
+ --timeout=<value> [default: 10000] Request timeout in milliseconds
1958
+ --total=<value> number to add to the total
1959
+
1960
+ OUTPUT FLAGS
1961
+ --csv Format output as csv
1962
+ --json Format output as json
1963
+ --markdown Format output as markdown table
1964
+ --[no-]simplify flatten and filter to output only the most important attributes, mostly relevant for json
1965
+
1966
+ DESCRIPTION
1967
+ Pull external counter and save it into a widget extra Supporter
1968
+
1969
+ EXAMPLES
1970
+ $ proca widget external --url https://mitmachen.wwf.de/node/506/polling
1971
+ ```
1972
+
1866
1973
  ## `proca widget get`
1867
1974
 
1868
1975
  view a widget
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proca",
3
3
  "description": "Access the proca api",
4
- "version": "2.2.0",
4
+ "version": "2.3.1",
5
5
  "author": "Xavier",
6
6
  "bin": {
7
7
  "proca": "proca-cli"
@@ -19,6 +19,7 @@
19
19
  "fast-csv": "^5.0.1",
20
20
  "graphql": "^16.10.0",
21
21
  "merge-anything": "^6.0.6",
22
+ "object-path": "^0.11.8",
22
23
  "prompts": "^2.4.2",
23
24
  "typescript": "^5.7.3",
24
25
  "urql": "^4.1.0"
@@ -74,6 +74,8 @@ export default class CampaignGet extends Command {
74
74
  Status: d.status,
75
75
  locales: d.config.locales && Object.keys(d.config.locales).join(" "),
76
76
  journey: d.config.journey?.join(" → "),
77
+ from: d.start?.substring(0, 10),
78
+ to: d.end?.substring(0, 10),
77
79
  };
78
80
  if (d.mtt) {
79
81
  // we have an mtt
@@ -83,8 +85,8 @@ export default class CampaignGet extends Command {
83
85
  minute: "2-digit",
84
86
  hour12: false,
85
87
  });
86
- result.from = d.mtt.startAt.substring(0, 10);
87
- result.to = d.mtt.endAt.substring(0, 10);
88
+ result["mtt from"] = d.mtt.startAt.substring(0, 10);
89
+ result["mttt to"] = d.mtt.endAt.substring(0, 10);
88
90
  result.period = `${hhmm(d.mtt.startAt)}↔${hhmm(d.mtt.endAt)}`;
89
91
  result["test email"] = d.mtt.testEmail;
90
92
  result["mtt template"] = d.mtt.template;
@@ -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
+ }
@@ -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,109 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Flags } from "@oclif/core";
3
+ import oPath from "object-path";
4
+ import { updateCounter } from "#src/commands/widget/update/external.mjs";
5
+ import Command from "#src/procaCommand.mjs";
6
+
7
+ export default class CounterExternal extends Command {
8
+ static description =
9
+ "Pull external counter and save it into a widget extra Supporter";
10
+
11
+ static examples = [
12
+ "<%= config.bin %> <%= command.id %> --url https://mitmachen.wwf.de/node/506/polling",
13
+ ];
14
+ static args = this.multiid();
15
+
16
+ static flags = {
17
+ ...this.flagify({ multiid: true }),
18
+ url: Flags.string({
19
+ char: "u",
20
+ description: "API endpoint URL to pull from",
21
+ relationships: [{ type: "some", flags: ["path", "dry-run"] }],
22
+ }),
23
+ path: Flags.string({
24
+ helpValue: "object.sub-object.total",
25
+ description:
26
+ "dot notation path to the counter field in the json returned by the url",
27
+ }),
28
+ total: Flags.integer({
29
+ description: "number to add to the total",
30
+ relationships: [
31
+ // define complex relationships between flags
32
+ { type: "none", flags: ["url", "path"] },
33
+ ],
34
+ }),
35
+ timeout: Flags.integer({
36
+ description: "Request timeout in milliseconds",
37
+ default: 10000,
38
+ }),
39
+ "dry-run": Flags.boolean({
40
+ description: "just fetch, don't update",
41
+ }),
42
+ };
43
+
44
+ async fetchCounter({ url, path, timeout, "dry-run": verbose }) {
45
+ const response = await fetch(url, {
46
+ timeout,
47
+ headers: {
48
+ "User-Agent": "proca",
49
+ },
50
+ });
51
+
52
+ if (!response.ok) {
53
+ this.error(`API request failed with status ${response.status}`, {
54
+ exit: 1,
55
+ });
56
+ }
57
+
58
+ const data = await response.json();
59
+ if (verbose) {
60
+ this.log(JSON.stringify(data, null, 2));
61
+ }
62
+ const counter = oPath.get(data, path);
63
+ if (Number.isNaN(Number.parseFloat(counter)) || !Number.isFinite(counter)) {
64
+ this.error(`Could not extract value from ${counter} at ${path}`, {
65
+ exit: 1,
66
+ });
67
+ }
68
+ return Number.parseFloat(counter);
69
+ }
70
+
71
+ async getCounterConfig(id) {
72
+ const folder = this.procaConfig.folder;
73
+ const filePath = `${folder}/${id}.json`;
74
+
75
+ const data = await readFile(filePath, "utf8");
76
+ const jsonData = JSON.parse(data);
77
+ return jsonData.component.counter;
78
+ }
79
+
80
+ async run() {
81
+ const { flags } = await this.parse(CounterExternal);
82
+ let counter = undefined;
83
+ if (!flags.url) {
84
+ const config = await this.getCounterConfig(flags.id);
85
+ flags.url = config.url;
86
+ flags.path = config.path;
87
+ }
88
+
89
+ if (flags.url) {
90
+ counter = await this.fetchCounter(flags);
91
+ }
92
+ if (flags.total) {
93
+ counter = flags.total;
94
+ }
95
+ if (flags["dry-run"]) {
96
+ return this.output(
97
+ {
98
+ counter,
99
+ url: flags.url,
100
+ // response: JSON.stringify(data, null, 2),
101
+ },
102
+ { single: true },
103
+ );
104
+ }
105
+
106
+ const updated = await updateCounter(flags.id, counter);
107
+ return this.output(updated, { single: true });
108
+ }
109
+ }
@@ -50,16 +50,18 @@ class ProcaCommand extends Command {
50
50
  return args;
51
51
  }
52
52
 
53
- static flagify({ multiid = false, name = false } = {}) {
53
+ static flagify({ multiid = false, name = false, char } = {}) {
54
54
  const flags = Object.assign({}, ProcaCommand.baseFlags);
55
55
  if (name || multiid) {
56
56
  flags.name = Flags.string({
57
57
  char: "n",
58
+ charAliases: char ? ["n", char] : undefined,
58
59
  description: "name (technical short name, also called slug)",
59
60
  helpValue: typeof name === "string" ? `<${name}>` : "<the_short_name>",
60
61
  parse: (input) => ProcaCommand.safeName(input),
61
62
  });
62
63
  }
64
+
63
65
  if (multiid) {
64
66
  flags.id = Flags.string({
65
67
  char: "i",
@@ -75,7 +77,7 @@ class ProcaCommand extends Command {
75
77
  }
76
78
 
77
79
  static safeName = (input) => {
78
- const pattern = /^[a-zA-Z0-9\-_]+$/;
80
+ const pattern = /^[a-zA-Z0-9\-_\/]+$/;
79
81
  if (!pattern.test(input)) {
80
82
  throw new Error(`Invalid characters in: ${input}`);
81
83
  }
@@ -157,6 +159,10 @@ class ProcaCommand extends Command {
157
159
  }
158
160
 
159
161
  if (err.networkError) {
162
+ if (err.response.status === 500) {
163
+ this.error("500 Internal Server Error", { exit: err.code || 1 });
164
+ return;
165
+ }
160
166
  this.info("Looks like there’s a problem with your internet connection");
161
167
  this.error(err.networkError.cause, { exit: err.code || 1 });
162
168
  }
@@ -286,7 +292,7 @@ class ProcaCommand extends Command {
286
292
  table(data, transformRow, print = (table) => table.toString()) {
287
293
  if (!transformRow) {
288
294
  if (this.flags.simplify !== false) {
289
- transformRow = (d, cell, idx) => {
295
+ transformRow = (d, cell) => {
290
296
  const r = this.simplify(d);
291
297
  if (r === null) return null;
292
298
  for (const [key, value] of Object.entries(r)) {
@@ -295,7 +301,7 @@ class ProcaCommand extends Command {
295
301
  return true;
296
302
  };
297
303
  } else {
298
- transformRow = (d, cell, idx) => {
304
+ transformRow = (d, cell) => {
299
305
  for (const [key, value] of Object.entries(this.flatten(d))) {
300
306
  cell(key, value);
301
307
  }
@@ -316,15 +322,18 @@ class ProcaCommand extends Command {
316
322
  }, this);
317
323
  return this.newRow();
318
324
  };
319
-
320
325
  this.log(Table.print(data, transformRow, print));
321
326
  }
322
327
 
323
328
  single = (r) => {
324
- this.table(r, null, null);
329
+ return this.table(r[0], null, null);
325
330
  };
326
331
 
327
332
  async output(data, { single = false } = {}) {
333
+ if (!Array.isArray(data)) {
334
+ data = [data];
335
+ single = true;
336
+ }
328
337
  if (this.format === "json") {
329
338
  if (this.flags.simplify)
330
339
  return data?.map(this.simplify) || this.simplify(data);
@@ -1,6 +1,6 @@
1
1
  import { gql } from "#src/urql.mjs";
2
2
 
3
- export const FragmentSummary = gql`fragment Summary on Campaign {id name title externalId status}`;
3
+ export const FragmentSummary = gql`fragment Summary on Campaign {id name title externalId status start end}`;
4
4
 
5
5
  export const FragmentMtt = gql`
6
6
  fragment Mtt on PrivateCampaign {
package/src/urql.mjs CHANGED
@@ -51,7 +51,6 @@ export const query = async (query, payload) => {
51
51
  export const mutation = async (mutation, payload) => {
52
52
  const result = await client.mutation(mutation, payload).toPromise();
53
53
  if (result.error) {
54
- console.log("error", result.error);
55
54
  throw result.error;
56
55
  }
57
56
  return result.data;