ps-filter 0.0.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/Procfile +1 -0
- package/README.md +22 -0
- package/contracts/filter.js +525 -0
- package/contracts/filter.json +10 -0
- package/img/logo25.png +0 -0
- package/package.json +34 -0
- package/src/api.js +37 -0
- package/src/utils/arweave.js +34 -0
- package/src/utils/cache.js +61 -0
- package/src/utils/constants.js +4 -0
- package/src/utils/filter.js +46 -0
- package/src/utils/getLastInteraction.js +58 -0
package/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: node ./src/api.js
|
package/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<a href="https://decent.land">
|
3
|
+
<img src="./img/logo25.png" height="124">
|
4
|
+
</a>
|
5
|
+
<h3 align="center"><code>@decentdotland/ps-filter</code></h3>
|
6
|
+
<p align="center">cache node & FE filter for the PublicSquare protocol</p>
|
7
|
+
</p>
|
8
|
+
|
9
|
+
## Synopsis
|
10
|
+
A cache node for the [filter layer](./contracts) (SmartWeave contract) of the PublicSquare protocol.
|
11
|
+
|
12
|
+
## API Endpoints
|
13
|
+
|
14
|
+
### 1- decoded cached feed
|
15
|
+
- Request: GET `/feeds`
|
16
|
+
- Response: Array of posts objects
|
17
|
+
|
18
|
+
## Further Documentation
|
19
|
+
TODO
|
20
|
+
|
21
|
+
## License
|
22
|
+
This project is licensed under the [MIT license](./LICENSE).
|
@@ -0,0 +1,525 @@
|
|
1
|
+
export async function handle(state, action) {
|
2
|
+
const input = action.input;
|
3
|
+
const caller = action.caller;
|
4
|
+
|
5
|
+
const rate_limit = state.rate_limit;
|
6
|
+
const post_char_limit = state.post_char_limit;
|
7
|
+
const representatives = state.representatives;
|
8
|
+
const super_representatives = state.super_representatives;
|
9
|
+
|
10
|
+
const ERROR_INVALID_NFT_SRC = "post's NFT SRC is not supported";
|
11
|
+
const ERROR_PID_ALREADY_EXIST = "PID already exits";
|
12
|
+
const ERROR_PID_NOT_EXIST = "the given PID not found";
|
13
|
+
const ERROR_INVALID_REPLY_TYPE = "reply's type must be of type 'post'";
|
14
|
+
const ERROR_USER_ADDRESS_NOT_EXIST =
|
15
|
+
"the given address has not interacted with the contract";
|
16
|
+
const ERROR_USER_ALREADY_SUSPENDED =
|
17
|
+
"user already suspended - duplicated action";
|
18
|
+
const ERROR_USER_NEVER_REPORTED =
|
19
|
+
"cannot suspend a user without at least a single report by a representative";
|
20
|
+
const ERROR_INVALID_CHAR_LIMIT =
|
21
|
+
"characters limit must be an integer greater than the min-safe limit";
|
22
|
+
const ERROR_INVALID_RATE_LIMIT =
|
23
|
+
"rate limit must be an integer less than the max-safe limit";
|
24
|
+
const ERROR_INVALID_INPUT = "the function has been given an invalid argument";
|
25
|
+
const ERROR_INVALID_PRIMITIVE_TYPE = "invalid primitive data type";
|
26
|
+
const ERROR_INVALID_STRING_LENGTH =
|
27
|
+
"the string surpass the allowed min-max limits";
|
28
|
+
const ERROR_CONTENT_BODY_IS_NOT_JSON =
|
29
|
+
"post content body is not a valid JSON";
|
30
|
+
const ERROR_INVALID_ARWEAVE_ADDRESS_TRANSACTION =
|
31
|
+
"the syntax of the string is not a valid Arweave address/TX";
|
32
|
+
const ERROR_POST_OWNER_NOT_CALLER = "the PID owner is not the caller";
|
33
|
+
const ERROR_INVALID_TX_TAG = "the TX has invalid TX tag(s)";
|
34
|
+
const ERROR_INVALID_POST_STRUCTURE = "PID data structure is not valid";
|
35
|
+
const ERROR_RATE_LIMIT = "user cannot post, has reached the rate limit";
|
36
|
+
const ERROR_USER_SUSPENDED = "user cannot post, has been suspended";
|
37
|
+
const ERROR_CALLER_NOT_REPRESENTATIVE =
|
38
|
+
"only a representative address can invoke this function";
|
39
|
+
const ERROR_REPORT_NOT_EXIST_OR_EXECUTED =
|
40
|
+
"report ID does not exist or the report has been executed";
|
41
|
+
const ERROR_CONTRACT_TEMPORARY_SEALED =
|
42
|
+
"the filter contract is sealed temporary - users interactions are revoked automatically";
|
43
|
+
|
44
|
+
if (input.function === "post") {
|
45
|
+
const txid = input.txid;
|
46
|
+
|
47
|
+
_checkSealing(state.is_sealed, caller);
|
48
|
+
_validateArweaveAddress(txid);
|
49
|
+
_checkPostDuplication(txid);
|
50
|
+
// get TX tags & validate post ownership
|
51
|
+
const tx_tags = await _getTransactionTags(txid, caller);
|
52
|
+
|
53
|
+
const post_src = state.nfts_src[0];
|
54
|
+
// const poll_src = state.nfts_src[1];
|
55
|
+
let post_type; // post or poll
|
56
|
+
|
57
|
+
_checkTagExistence(tx_tags, "App-Name", "SmartWeaveContract");
|
58
|
+
_checkTagExistence(tx_tags, "App-Version", "0.3.0");
|
59
|
+
_checkTagExistence(tx_tags, "Content-Type", "application/json");
|
60
|
+
_checkTagExistence(tx_tags, "Protocol-Name", "DecentLand");
|
61
|
+
_checkTagExistence(tx_tags, "Protocol-Action", "post");
|
62
|
+
_checkTagExistence(tx_tags, "Tribus-ID", SmartWeave.contract.id);
|
63
|
+
|
64
|
+
if (
|
65
|
+
!tx_tags["Contract-Src"] ||
|
66
|
+
!state.nfts_src.includes(tx_tags["Contract-Src"])
|
67
|
+
) {
|
68
|
+
throw new ContractError(ERROR_INVALID_NFT_SRC);
|
69
|
+
}
|
70
|
+
|
71
|
+
await _checkPostStructure(txid);
|
72
|
+
|
73
|
+
// if (tx_tags["Contract-Src"] === poll_src) {
|
74
|
+
// await _checkPollStructure(txid);
|
75
|
+
// post_type = "poll"
|
76
|
+
// }
|
77
|
+
|
78
|
+
if (!(caller in state.users)) {
|
79
|
+
state.users[caller] = {
|
80
|
+
last_interaction: 0,
|
81
|
+
reports_count: 0,
|
82
|
+
user_status: "OK",
|
83
|
+
};
|
84
|
+
}
|
85
|
+
|
86
|
+
_checkUserStatus(caller);
|
87
|
+
_checkUserRateLimit(caller);
|
88
|
+
|
89
|
+
state.users[caller].last_interaction = SmartWeave.block.height;
|
90
|
+
|
91
|
+
state.feed.push({
|
92
|
+
pid: txid,
|
93
|
+
type: "post", // hardcoded
|
94
|
+
owner: caller,
|
95
|
+
timestamp: SmartWeave.block.timestamp,
|
96
|
+
replies: [],
|
97
|
+
});
|
98
|
+
|
99
|
+
return { state };
|
100
|
+
}
|
101
|
+
|
102
|
+
if (input.function === "reply") {
|
103
|
+
const txid = input.txid;
|
104
|
+
const post_id = input.post_id;
|
105
|
+
|
106
|
+
_checkSealing(state.is_sealed, caller);
|
107
|
+
_validateArweaveAddress(txid);
|
108
|
+
_checkPostDuplication(txid);
|
109
|
+
|
110
|
+
const post_id_index = state.feed.findIndex((post) => post.pid === post_id);
|
111
|
+
const post_type = _getPostType(post_id);
|
112
|
+
|
113
|
+
if (post_id_index === -1) {
|
114
|
+
throw new ContractError(ERROR_PID_NOT_EXIST);
|
115
|
+
}
|
116
|
+
|
117
|
+
// get TX tags & validate post ownership
|
118
|
+
const tx_tags = await _getTransactionTags(txid, caller);
|
119
|
+
|
120
|
+
const post_src = state.nfts_src[0];
|
121
|
+
|
122
|
+
_checkTagExistence(tx_tags, "App-Name", "SmartWeaveContract");
|
123
|
+
_checkTagExistence(tx_tags, "App-Version", "0.3.0");
|
124
|
+
_checkTagExistence(tx_tags, "Content-Type", "application/json");
|
125
|
+
_checkTagExistence(tx_tags, "Protocol-Name", "DecentLand");
|
126
|
+
_checkTagExistence(tx_tags, "Protocol-Action", "reply");
|
127
|
+
_checkTagExistence(tx_tags, "reply_to", post_id);
|
128
|
+
_checkTagExistence(tx_tags, "Tribus-ID", SmartWeave.contract.id);
|
129
|
+
|
130
|
+
if (
|
131
|
+
!tx_tags["Contract-Src"] ||
|
132
|
+
!state.nfts_src.includes(tx_tags["Contract-Src"])
|
133
|
+
) {
|
134
|
+
throw new ContractError(ERROR_INVALID_NFT_SRC);
|
135
|
+
}
|
136
|
+
|
137
|
+
// reply can be type of 'post' only
|
138
|
+
if (tx_tags["Contract-Src"] !== post_src) {
|
139
|
+
throw new ContractError(ERROR_INVALID_REPLY_TYPE);
|
140
|
+
}
|
141
|
+
|
142
|
+
await _checkPostStructure(txid);
|
143
|
+
|
144
|
+
if (!(caller in state.users)) {
|
145
|
+
state.users[caller] = {
|
146
|
+
last_interaction: 0,
|
147
|
+
reports_count: 0,
|
148
|
+
user_status: "OK",
|
149
|
+
};
|
150
|
+
}
|
151
|
+
|
152
|
+
_checkUserStatus(caller);
|
153
|
+
_checkUserRateLimit(caller);
|
154
|
+
|
155
|
+
state.users[caller].last_interaction = SmartWeave.block.height;
|
156
|
+
|
157
|
+
state.feed[post_id_index]["replies"].push({
|
158
|
+
pid: txid,
|
159
|
+
childOf: post_id,
|
160
|
+
owner: caller,
|
161
|
+
timestamp: SmartWeave.block.timestamp,
|
162
|
+
});
|
163
|
+
|
164
|
+
return { state };
|
165
|
+
}
|
166
|
+
|
167
|
+
// REPRESENTATIVES ACTIONS
|
168
|
+
if (input.function === "report_post") {
|
169
|
+
const pid = input.pid;
|
170
|
+
const message = input.message;
|
171
|
+
|
172
|
+
_isRepresentative(caller);
|
173
|
+
|
174
|
+
const is_post = state.feed.find((post) => post["pid"] === pid);
|
175
|
+
|
176
|
+
if (!is_post) {
|
177
|
+
throw new ContractError(ERROR_PID_NOT_EXIST);
|
178
|
+
}
|
179
|
+
|
180
|
+
const report_index = state.reports.findIndex(
|
181
|
+
(report) =>
|
182
|
+
report.type === "post_report" &&
|
183
|
+
report["pid"] === pid &&
|
184
|
+
!report?.reporters?.includes(caller)
|
185
|
+
);
|
186
|
+
|
187
|
+
if (report_index !== -1) {
|
188
|
+
const report = state.reports[report_index];
|
189
|
+
report.reporters.push(caller);
|
190
|
+
reports_count += 1;
|
191
|
+
|
192
|
+
return { state };
|
193
|
+
}
|
194
|
+
|
195
|
+
state.reports.push({
|
196
|
+
report_id: SmartWeave.transaction.id,
|
197
|
+
pid: pid,
|
198
|
+
type: "post_report",
|
199
|
+
message: message,
|
200
|
+
reporters: [caller],
|
201
|
+
reports_count: 0,
|
202
|
+
});
|
203
|
+
|
204
|
+
return { state };
|
205
|
+
}
|
206
|
+
// SUPER REPRESENTATIVE ACTIONS
|
207
|
+
if (input.function === "execute_report") {
|
208
|
+
const report_id = input.report_id;
|
209
|
+
|
210
|
+
_isSuperRepresentative(caller);
|
211
|
+
|
212
|
+
const report_index = _getReportIndex(report_id);
|
213
|
+
const report = state.reports[report_index];
|
214
|
+
|
215
|
+
if (report["type"] === "post_report") {
|
216
|
+
const pid = report["pid"];
|
217
|
+
const post_index = state.feed.findIndex((post) => post["pid"] === pid);
|
218
|
+
const post_owner = state.feed[post_index]["owner"];
|
219
|
+
|
220
|
+
state.feed.splice(post_index, 1);
|
221
|
+
state.reports[report_index].status = "executed";
|
222
|
+
state.users[post_owner]["reports_count"] += 1;
|
223
|
+
|
224
|
+
return { state };
|
225
|
+
}
|
226
|
+
|
227
|
+
if (report["type"] === "reply_report") {
|
228
|
+
const pid = report["pid"];
|
229
|
+
const post_index = state.feed.findIndex((post) =>
|
230
|
+
post["replies"].find((reply) => reply["pid"] === pid)
|
231
|
+
);
|
232
|
+
const reply_index = state.feed[post_index]["replies"].find(
|
233
|
+
(reply) => reply["pid"] === pid
|
234
|
+
);
|
235
|
+
const post_owner =
|
236
|
+
state.feed[post_index]["replies"][reply_index]["owner"];
|
237
|
+
|
238
|
+
state.feed[post_index]["replies"].splice(reply_index, 1);
|
239
|
+
state.reports[report_index].status = "executed";
|
240
|
+
state.users[post_owner]["reports_count"] += 1;
|
241
|
+
|
242
|
+
return { state };
|
243
|
+
}
|
244
|
+
|
245
|
+
return { state };
|
246
|
+
}
|
247
|
+
|
248
|
+
if (input.function === "suspend_user") {
|
249
|
+
const user_address = input.user_address;
|
250
|
+
|
251
|
+
_validateArweaveAddress(user_address);
|
252
|
+
_isSuperRepresentative(caller);
|
253
|
+
|
254
|
+
if (!state.users.user_address) {
|
255
|
+
throw new ContractError(ERROR_USER_ADDRESS_NOT_EXIST);
|
256
|
+
}
|
257
|
+
|
258
|
+
if (state.users.user_address.user_status !== "OK") {
|
259
|
+
throw new ContractError(ERROR_USER_ALREADY_SUSPENDED);
|
260
|
+
}
|
261
|
+
|
262
|
+
const user_reports_count = state.users.user_address.reports_count;
|
263
|
+
const user_super_reports =
|
264
|
+
state.users.user_address?.super_rep_reports_count;
|
265
|
+
const super_rep_half_plus_one =
|
266
|
+
Math.trunc(state.super_representatives.length / 2) + 1;
|
267
|
+
|
268
|
+
// user must be reported by atleast a single representative
|
269
|
+
if (user_reports_count === 0) {
|
270
|
+
throw new ContractError(ERROR_USER_NEVER_REPORTED);
|
271
|
+
}
|
272
|
+
|
273
|
+
if (!user_super_reports) {
|
274
|
+
state.users.user_address.super_rep_reports_count = 1;
|
275
|
+
// executed if sup_rep count = 1
|
276
|
+
if (user_super_reports >= super_rep_half_plus_one) {
|
277
|
+
state.users.user_address.user_status = "SUSPENDED";
|
278
|
+
return { state };
|
279
|
+
}
|
280
|
+
return { state };
|
281
|
+
}
|
282
|
+
|
283
|
+
if (user_super_reports < super_rep_half_plus_one) {
|
284
|
+
state.users.user_address.super_rep_reports_count += 1;
|
285
|
+
|
286
|
+
if (user_super_reports >= super_rep_half_plus_one) {
|
287
|
+
state.users.user_address.user_status = "SUSPENDED";
|
288
|
+
return { state };
|
289
|
+
}
|
290
|
+
|
291
|
+
return { state };
|
292
|
+
}
|
293
|
+
state.users.user_address.super_rep_reports_count += 1;
|
294
|
+
state.users.user_address.user_status = "SUSPENDED";
|
295
|
+
|
296
|
+
return { state };
|
297
|
+
}
|
298
|
+
|
299
|
+
if (input.function === "edit_characters_limit") {
|
300
|
+
const new_char_limit = input.new_char_limit;
|
301
|
+
const safe_minimum_limit = 280;
|
302
|
+
|
303
|
+
_isSuperRepresentative(caller);
|
304
|
+
|
305
|
+
if (
|
306
|
+
!Number.isInteger(new_char_limit) ||
|
307
|
+
new_char_limit < safe_minimum_limit
|
308
|
+
) {
|
309
|
+
throw new ContractError(ERROR_INVALID_CHAR_LIMIT);
|
310
|
+
}
|
311
|
+
|
312
|
+
state.post_char_limit = new_limit;
|
313
|
+
|
314
|
+
return { state };
|
315
|
+
}
|
316
|
+
|
317
|
+
if (input.function === "edit_rate_limit") {
|
318
|
+
const new_rate_limit = input.new_rate_limit;
|
319
|
+
// 30 blocks delay between every interaction;
|
320
|
+
const safe_maximum_limit = 30;
|
321
|
+
|
322
|
+
_isSuperRepresentative(caller);
|
323
|
+
|
324
|
+
if (!Number.isInteger(new_rate_limit) || new_limit > safe_maximum_limit) {
|
325
|
+
throw new ContractError(ERROR_INVALID_RATE_LIMIT);
|
326
|
+
}
|
327
|
+
|
328
|
+
state.rate_limit = new_rate_limit;
|
329
|
+
|
330
|
+
return { state };
|
331
|
+
}
|
332
|
+
|
333
|
+
if (input.function === "edit_sealing") {
|
334
|
+
const sealing = input.sealing;
|
335
|
+
|
336
|
+
_isSuperRepresentative(caller);
|
337
|
+
|
338
|
+
if (![true, false].includes(sealing)) {
|
339
|
+
throw new ContractError(ERROR_INVALID_INPUT);
|
340
|
+
}
|
341
|
+
|
342
|
+
state.is_sealed = sealing;
|
343
|
+
|
344
|
+
return { state };
|
345
|
+
}
|
346
|
+
|
347
|
+
if (input.function === "remove_representative") {
|
348
|
+
const address = input.address;
|
349
|
+
|
350
|
+
_isSuperRepresentative(caller);
|
351
|
+
_isRepresentative(address);
|
352
|
+
|
353
|
+
const representativeIndex = state.representatives.findIndex(address);
|
354
|
+
state.representatives.splice(representativeIndex, 1);
|
355
|
+
|
356
|
+
return { state };
|
357
|
+
}
|
358
|
+
|
359
|
+
// HELPER FUNCTIONS
|
360
|
+
function _validateStringTypeLen(str, minLen, maxLen) {
|
361
|
+
if (typeof str !== "string") {
|
362
|
+
throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
|
363
|
+
}
|
364
|
+
|
365
|
+
if (str.length < minLen || str.length > maxLen) {
|
366
|
+
throw new ContractError(ERROR_INVALID_STRING_LENGTH);
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
370
|
+
function _validateArweaveAddress(str) {
|
371
|
+
_validateStringTypeLen(str, 43, 43);
|
372
|
+
|
373
|
+
const validity = /[a-z0-9_-]{43}/i.test(str);
|
374
|
+
if (!validity) {
|
375
|
+
throw new ContractError(ERROR_INVALID_ARWEAVE_ADDRESS_TRANSACTION);
|
376
|
+
}
|
377
|
+
}
|
378
|
+
|
379
|
+
async function _getTransactionTags(txid, address) {
|
380
|
+
const tags = {};
|
381
|
+
const tx_object = await SmartWeave.unsafeClient.transactions.get(txid);
|
382
|
+
const tx_owner_decoded =
|
383
|
+
await SmartWeave.unsafeClient.wallets.ownerToAddress(tx_object.owner);
|
384
|
+
const tx_tags = tx_object.get("tags");
|
385
|
+
|
386
|
+
for (let tag of tx_tags) {
|
387
|
+
const key = tag.get("name", { decode: true, string: true });
|
388
|
+
const value = tag.get("value", { decode: true, string: true });
|
389
|
+
|
390
|
+
tags[key] = value;
|
391
|
+
}
|
392
|
+
|
393
|
+
if (tx_owner_decoded !== address) {
|
394
|
+
throw new ContractError(ERROR_POST_OWNER_NOT_CALLER);
|
395
|
+
}
|
396
|
+
|
397
|
+
return tags;
|
398
|
+
}
|
399
|
+
|
400
|
+
function _checkTagExistence(tags_object, key, value) {
|
401
|
+
if (!(key in tags_object) || tags_object[key] !== value) {
|
402
|
+
throw new ContractError(ERROR_INVALID_TX_TAG);
|
403
|
+
}
|
404
|
+
}
|
405
|
+
|
406
|
+
function _checkPostDuplication(txid) {
|
407
|
+
const post_existence = state.feed.find((post) => post.pid === txid);
|
408
|
+
const reply_existence = state.feed.find((post) =>
|
409
|
+
post["replies"].find((reply) => reply.pid === txid)
|
410
|
+
);
|
411
|
+
|
412
|
+
if (post_existence || reply_existence) {
|
413
|
+
throw new ContractError(ERROR_PID_ALREADY_EXIST);
|
414
|
+
}
|
415
|
+
}
|
416
|
+
|
417
|
+
function _get_data_type(data) {
|
418
|
+
return Object.prototype.toString.call(data);
|
419
|
+
}
|
420
|
+
|
421
|
+
async function _checkPostStructure(txid) {
|
422
|
+
const content_string = await SmartWeave.unsafeClient.transactions.getData(
|
423
|
+
txid,
|
424
|
+
{ decode: true, string: true }
|
425
|
+
);
|
426
|
+
|
427
|
+
try {
|
428
|
+
JSON.parse(content_string);
|
429
|
+
} catch (error) {
|
430
|
+
throw new ContractError(ERROR_CONTENT_BODY_IS_NOT_JSON);
|
431
|
+
}
|
432
|
+
|
433
|
+
const content_object = JSON.parse(content_string);
|
434
|
+
|
435
|
+
const isObject = _get_data_type(content_object) === "[object Object]";
|
436
|
+
const hasContent =
|
437
|
+
content_object.content &&
|
438
|
+
_get_data_type(content_object.content) === "[object String]";
|
439
|
+
const hasMediaArray =
|
440
|
+
content_object.media &&
|
441
|
+
_get_data_type(content_object.media) === "[object Array]";
|
442
|
+
const hasOnlyContentAndMedia = Object.keys(content_object).length === 2;
|
443
|
+
const isEmptyPost =
|
444
|
+
content_object?.content?.length + content_object?.media?.length === 0;
|
445
|
+
const isContentBelowLimit =
|
446
|
+
content_object.content?.length < post_char_limit;
|
447
|
+
|
448
|
+
if (
|
449
|
+
isObject &&
|
450
|
+
hasContent &&
|
451
|
+
hasMediaArray &&
|
452
|
+
hasOnlyContentAndMedia &&
|
453
|
+
isContentBelowLimit &&
|
454
|
+
!isEmptyPost
|
455
|
+
) {
|
456
|
+
return true;
|
457
|
+
}
|
458
|
+
|
459
|
+
throw new ContractError(ERROR_INVALID_POST_STRUCTURE);
|
460
|
+
}
|
461
|
+
|
462
|
+
function _checkUserRateLimit(address) {
|
463
|
+
const current_blockheight = SmartWeave.block.height;
|
464
|
+
const current_user_blockheight = state.users[address].last_interaction;
|
465
|
+
const get_rate_limit = state.users[address]["rate_limit"]
|
466
|
+
? state.users[address]["rate_limit"]
|
467
|
+
: state.rate_limit;
|
468
|
+
|
469
|
+
if (!(current_user_blockheight < current_blockheight + get_rate_limit)) {
|
470
|
+
throw new ContractError(ERROR_RATE_LIMIT);
|
471
|
+
}
|
472
|
+
}
|
473
|
+
|
474
|
+
function _checkUserStatus(address) {
|
475
|
+
const status = state.users[address].user_status;
|
476
|
+
|
477
|
+
if (status !== "OK") {
|
478
|
+
throw new ContractError(ERROR_USER_SUSPENDED);
|
479
|
+
}
|
480
|
+
}
|
481
|
+
|
482
|
+
function _isRepresentative(address) {
|
483
|
+
const is_rep = representatives.includes(address);
|
484
|
+
|
485
|
+
if (!is_rep) {
|
486
|
+
throw new ContractError(ERROR_CALLER_NOT_REPRESENTATIVE);
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
function _isSuperRepresentative(address) {
|
491
|
+
const is_rep = super_representatives.includes(address);
|
492
|
+
|
493
|
+
if (!is_rep) {
|
494
|
+
throw new ContractError(ERROR_CALLER_NOT_REPRESENTATIVE);
|
495
|
+
}
|
496
|
+
}
|
497
|
+
|
498
|
+
function _getReportIndex(id) {
|
499
|
+
// report.status is defined when the report get executed
|
500
|
+
// not executed report have no status
|
501
|
+
const index = state.reports.findIndex(
|
502
|
+
(report) => report["report_id"] === id && report.status
|
503
|
+
);
|
504
|
+
|
505
|
+
if (index === -1) {
|
506
|
+
throw new ContractError(ERROR_REPORT_NOT_EXIST_OR_EXECUTED);
|
507
|
+
}
|
508
|
+
}
|
509
|
+
|
510
|
+
function _getPostType(post_id) {
|
511
|
+
const post_index = state.feed.findIndex((post) => post.pid === post_id);
|
512
|
+
if (post_index === -1) {
|
513
|
+
throw new ContractError(ERROR_PID_NOT_EXIST);
|
514
|
+
}
|
515
|
+
|
516
|
+
return state.feed[post_index].type;
|
517
|
+
}
|
518
|
+
|
519
|
+
function _checkSealing(filter_sealing_state, address) {
|
520
|
+
if (filter_sealing_state && !(address in state.users)) {
|
521
|
+
throw new ContractError(ERROR_CONTRACT_TEMPORARY_SEALED);
|
522
|
+
}
|
523
|
+
}
|
524
|
+
}
|
525
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{ "nfts_src": ["I8xgq3361qpR8_DvqcGpkCYAUTMktyAgvkm6kGhJzEQ"],
|
2
|
+
"rate_limit": 0,
|
3
|
+
"post_char_limit": 750,
|
4
|
+
"is_sealed": false,
|
5
|
+
"feed": [],
|
6
|
+
"users": {},
|
7
|
+
"representatives": ["vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0"],
|
8
|
+
"super_representatives": ["vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0"],
|
9
|
+
"reports": []
|
10
|
+
}
|
package/img/logo25.png
ADDED
Binary file
|
package/package.json
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"name": "ps-filter",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"type": "module",
|
5
|
+
"description": "a cache node for the filter-layer of the PublicSquare protocol",
|
6
|
+
"main": "./src/api.js",
|
7
|
+
"scripts": {
|
8
|
+
"start": "node ./src/api.js"
|
9
|
+
},
|
10
|
+
"repository": {
|
11
|
+
"type": "git",
|
12
|
+
"url": "git+https://github.com/decentldotland/ps-filter.git"
|
13
|
+
},
|
14
|
+
"bugs": {
|
15
|
+
"url": "https://github.com/decentldotland/ps-filter/issues"
|
16
|
+
},
|
17
|
+
"keywords": [
|
18
|
+
"public-square",
|
19
|
+
"web3",
|
20
|
+
"arweave",
|
21
|
+
"social"
|
22
|
+
],
|
23
|
+
"author": "charmful0x",
|
24
|
+
"license": "MIT",
|
25
|
+
"dependencies": {
|
26
|
+
"arweave": "^1.10.23",
|
27
|
+
"axios": "^0.26.0",
|
28
|
+
"base64url": "^3.0.1",
|
29
|
+
"cors": "^2.8.5",
|
30
|
+
"express": "^4.17.3",
|
31
|
+
"node-cache": "^5.1.2",
|
32
|
+
"redstone-smartweave": "^0.4.71"
|
33
|
+
}
|
34
|
+
}
|
package/src/api.js
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
import express from "express";
|
2
|
+
import base64url from "base64url";
|
3
|
+
import cors from "cors";
|
4
|
+
import { getFeed, cache } from "./utils/cache.js";
|
5
|
+
import { timeout } from "./utils/arweave.js";
|
6
|
+
|
7
|
+
const app = express();
|
8
|
+
const port = process.env.PORT || 7777;
|
9
|
+
|
10
|
+
app.use(
|
11
|
+
cors({
|
12
|
+
origin: "*",
|
13
|
+
})
|
14
|
+
);
|
15
|
+
|
16
|
+
app.get("/feeds", async (req, res) => {
|
17
|
+
res.setHeader("Content-Type", "application/json");
|
18
|
+
const encodedFeed = await getFeed();
|
19
|
+
const jsonRes = JSON.parse(base64url.decode(encodedFeed));
|
20
|
+
res.send(jsonRes);
|
21
|
+
});
|
22
|
+
|
23
|
+
app.listen(port, async () => {
|
24
|
+
await polling();
|
25
|
+
console.log(`listening at PORT:${port}`);
|
26
|
+
});
|
27
|
+
|
28
|
+
async function polling() {
|
29
|
+
while (true) {
|
30
|
+
try {
|
31
|
+
await cache();
|
32
|
+
await timeout();
|
33
|
+
} catch (error) {
|
34
|
+
console.log(error);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import Arweave from "arweave";
|
2
|
+
import { SmartWeaveNodeFactory } from "redstone-smartweave";
|
3
|
+
import { FILTER_SWC_ADDRESS, BLOCK_SLEEPING_TIMEOUT } from "./constants.js";
|
4
|
+
|
5
|
+
export const arweave = Arweave.init({
|
6
|
+
host: "arweave.net",
|
7
|
+
port: 443,
|
8
|
+
protocol: "https",
|
9
|
+
timeout: 20000,
|
10
|
+
logging: false,
|
11
|
+
});
|
12
|
+
|
13
|
+
const smartweave = SmartWeaveNodeFactory.memCached(arweave);
|
14
|
+
|
15
|
+
export async function readFilterContract() {
|
16
|
+
try {
|
17
|
+
const contract = smartweave.contract(FILTER_SWC_ADDRESS);
|
18
|
+
const { state, validity } = await contract.readState();
|
19
|
+
|
20
|
+
return state;
|
21
|
+
} catch (error) {
|
22
|
+
console.log(error);
|
23
|
+
return false;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
export function timeout() {
|
28
|
+
// a block is ~ 2 min
|
29
|
+
const ms = BLOCK_SLEEPING_TIMEOUT * 2 * 60 * 1e3;
|
30
|
+
console.log(
|
31
|
+
`\nsleeping for ${BLOCK_SLEEPING_TIMEOUT} network blocks or ${ms} ms\n`
|
32
|
+
);
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
34
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { readFilterContract, arweave } from "./arweave.js";
|
2
|
+
import { readAndEncode } from "./filter.js";
|
3
|
+
import { getLastInteraction } from "./getLastInteraction.js";
|
4
|
+
import NodeCache from "node-cache";
|
5
|
+
import base64url from "base64url";
|
6
|
+
|
7
|
+
const base64Cache = new NodeCache();
|
8
|
+
|
9
|
+
export async function cache() {
|
10
|
+
const lastInteraction = (await getLastInteraction())[0]?.block;
|
11
|
+
const currentBlock = (await arweave.blocks.getCurrent())?.height;
|
12
|
+
|
13
|
+
// initialization
|
14
|
+
if (!base64Cache.has("feed")) {
|
15
|
+
const feed = await readAndEncode();
|
16
|
+
base64Cache.set("raw", feed.raw);
|
17
|
+
base64Cache.set("feed", feed.loaded);
|
18
|
+
base64Cache.set("height", currentBlock);
|
19
|
+
|
20
|
+
console.log(`STATE ALREADY CACHED - HEIGHT: ${base64Cache.get("height")}`);
|
21
|
+
}
|
22
|
+
|
23
|
+
// cache new interactions
|
24
|
+
if (
|
25
|
+
!base64Cache.get("height") ||
|
26
|
+
base64Cache.get("height") < lastInteraction
|
27
|
+
) {
|
28
|
+
const feed = await readAndEncode();
|
29
|
+
base64Cache.set("raw", feed.raw);
|
30
|
+
base64Cache.set("feed", feed.loaded);
|
31
|
+
base64Cache.set("height", lastInteraction);
|
32
|
+
|
33
|
+
console.log(`NEW STATE CACHED - HEIGHT: ${lastInteraction}`);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
export async function getPidData(pid) {
|
38
|
+
try {
|
39
|
+
// feed is passed by readAndEncode();
|
40
|
+
if (base64Cache.has(pid)) {
|
41
|
+
return base64Cache.get(pid);
|
42
|
+
}
|
43
|
+
// if not cached, cache it
|
44
|
+
const contentData = await arweave.transactions.getData(pid, {
|
45
|
+
decode: true,
|
46
|
+
string: true,
|
47
|
+
});
|
48
|
+
base64Cache.set(pid, contentData);
|
49
|
+
return base64Cache.get(pid);
|
50
|
+
} catch (error) {
|
51
|
+
console.log(error);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
export async function getFeed() {
|
56
|
+
if (!base64Cache.has("feed")) {
|
57
|
+
return "e30";
|
58
|
+
}
|
59
|
+
|
60
|
+
return base64Cache.get("feed");
|
61
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { readFilterContract } from "./arweave.js";
|
2
|
+
import { getPidData } from "./cache.js";
|
3
|
+
import base64url from "base64url";
|
4
|
+
|
5
|
+
export async function readAndEncode() {
|
6
|
+
try {
|
7
|
+
const feed0 = (await readFilterContract()).feed;
|
8
|
+
const feed1 = await getFeed1(feed0);
|
9
|
+
const feed2 = await getFeed2(feed1);
|
10
|
+
|
11
|
+
return {
|
12
|
+
loaded: base64url(JSON.stringify(feed2)), // post's TXID (data) is decoded
|
13
|
+
raw: base64url(JSON.stringify(feed0)) // post content is encoded as Arweave TXID
|
14
|
+
}
|
15
|
+
} catch (error) {
|
16
|
+
console.log(error);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
async function getFeed1(feed) {
|
21
|
+
try {
|
22
|
+
for (let thread of feed) {
|
23
|
+
thread.pid = await getPidData(thread.pid);
|
24
|
+
}
|
25
|
+
|
26
|
+
return feed;
|
27
|
+
} catch (error) {
|
28
|
+
console.log(error);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
async function getFeed2(feed) {
|
33
|
+
try {
|
34
|
+
for (let thread of feed) {
|
35
|
+
if (thread.replies.length > 0) {
|
36
|
+
for (let reply of thread.replies) {
|
37
|
+
reply.pid = await getPidData(reply.pid);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
return feed;
|
43
|
+
} catch (error) {
|
44
|
+
console.log(error);
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import axios from "axios";
|
2
|
+
import { FILTER_SWC_ADDRESS } from "./constants.js";
|
3
|
+
import { arweave } from "./arweave.js";
|
4
|
+
|
5
|
+
const gqlQuery = {
|
6
|
+
query: `query {
|
7
|
+
transactions(
|
8
|
+
tags: [
|
9
|
+
{ name: "App-Name", values: "SmartWeaveAction"},
|
10
|
+
{ name: "Contract", values: "${FILTER_SWC_ADDRESS}"}
|
11
|
+
]
|
12
|
+
first: 1
|
13
|
+
) {
|
14
|
+
edges {
|
15
|
+
node {
|
16
|
+
id
|
17
|
+
owner { address }
|
18
|
+
block { height }
|
19
|
+
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}`,
|
24
|
+
};
|
25
|
+
|
26
|
+
async function gqlTemplate(query) {
|
27
|
+
const response = await axios.post("https://arweave.net/graphql", query, {
|
28
|
+
headers: { "Content-Type": "application/json" },
|
29
|
+
});
|
30
|
+
|
31
|
+
const transactionIds = [];
|
32
|
+
|
33
|
+
const res_arr = response.data.data.transactions.edges;
|
34
|
+
|
35
|
+
for (let element of res_arr) {
|
36
|
+
const tx = element["node"];
|
37
|
+
const txExistence = transactionIds.find((txObj) => txObj.id === tx.id);
|
38
|
+
|
39
|
+
if (!txExistence) {
|
40
|
+
transactionIds.push({
|
41
|
+
id: tx.id,
|
42
|
+
owner: tx.owner.address,
|
43
|
+
block: tx.block ? tx.block.height : void 0,
|
44
|
+
});
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
return transactionIds;
|
49
|
+
}
|
50
|
+
|
51
|
+
export async function getLastInteraction() {
|
52
|
+
try {
|
53
|
+
const re = await gqlTemplate(gqlQuery);
|
54
|
+
return re;
|
55
|
+
} catch (error) {
|
56
|
+
console.log(error);
|
57
|
+
}
|
58
|
+
}
|