@fsvreddit/fsv-devvit-web-helpers 0.1.9
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/LICENSE +28 -0
- package/README.md +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/redditAPI/index.d.ts +3 -0
- package/dist/redditAPI/index.js +19 -0
- package/dist/redditAPI/postsAndComments.d.ts +3 -0
- package/dist/redditAPI/postsAndComments.js +16 -0
- package/dist/redditAPI/users.d.ts +2 -0
- package/dist/redditAPI/users.js +14 -0
- package/dist/redditAPI/wiki.d.ts +2 -0
- package/dist/redditAPI/wiki.js +23 -0
- package/dist/redis/index.d.ts +1 -0
- package/dist/redis/index.js +17 -0
- package/dist/redis/redisHelpers.d.ts +49 -0
- package/dist/redis/redisHelpers.js +122 -0
- package/dist/tasks/index.d.ts +1 -0
- package/dist/tasks/index.js +17 -0
- package/dist/tasks/upgradeNotifier.d.ts +8 -0
- package/dist/tasks/upgradeNotifier.js +80 -0
- package/dist/triggers/duplicateTriggerMitigation.d.ts +6 -0
- package/dist/triggers/duplicateTriggerMitigation.js +13 -0
- package/dist/triggers/index.d.ts +2 -0
- package/dist/triggers/index.js +18 -0
- package/dist/triggers/postsAndComments.d.ts +5 -0
- package/dist/triggers/postsAndComments.js +67 -0
- package/eslint.config.mjs +3 -0
- package/package.json +46 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, fsv
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Helper utility code for Devvit apps made by u/fsv
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./redditAPI/index.js"), exports);
|
|
18
|
+
__exportStar(require("./tasks/index.js"), exports);
|
|
19
|
+
__exportStar(require("./triggers/index.js"), exports);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./postsAndComments.js"), exports);
|
|
18
|
+
__exportStar(require("./users.js"), exports);
|
|
19
|
+
__exportStar(require("./wiki.js"), exports);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPostOrCommentById = getPostOrCommentById;
|
|
4
|
+
const server_1 = require("@devvit/web/server");
|
|
5
|
+
const shared_1 = require("@devvit/web/shared");
|
|
6
|
+
function getPostOrCommentById(thingId) {
|
|
7
|
+
if ((0, shared_1.isT1)(thingId)) {
|
|
8
|
+
return server_1.reddit.getCommentById(thingId);
|
|
9
|
+
}
|
|
10
|
+
else if ((0, shared_1.isT3)(thingId)) {
|
|
11
|
+
return server_1.reddit.getPostById(thingId);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
throw new Error(`Invalid thingId ${thingId}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getUserOrUndefined = getUserOrUndefined;
|
|
4
|
+
const server_1 = require("@devvit/web/server");
|
|
5
|
+
async function getUserOrUndefined(username) {
|
|
6
|
+
let user;
|
|
7
|
+
try {
|
|
8
|
+
user = await server_1.reddit.getUserByUsername(username);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
//
|
|
12
|
+
}
|
|
13
|
+
return user;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateWikiPageMulti = updateWikiPageMulti;
|
|
4
|
+
const server_1 = require("@devvit/web/server");
|
|
5
|
+
const marked_1 = require("marked");
|
|
6
|
+
async function updateWikiPageMulti(options) {
|
|
7
|
+
const isV2WikiEnabled = await server_1.reddit.isWikiV2Enabled(server_1.context.subredditName);
|
|
8
|
+
const promises = [
|
|
9
|
+
server_1.reddit.updateWikiPage({
|
|
10
|
+
...options,
|
|
11
|
+
wikiVersion: "v1",
|
|
12
|
+
}),
|
|
13
|
+
];
|
|
14
|
+
if (isV2WikiEnabled) {
|
|
15
|
+
const contentHtml = await Promise.resolve((0, marked_1.marked)(options.content));
|
|
16
|
+
promises.push(server_1.reddit.updateWikiPage({
|
|
17
|
+
...options,
|
|
18
|
+
content: contentHtml,
|
|
19
|
+
wikiVersion: "v2",
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
return Promise.all(promises).then(results => results[0]);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./redisHelpers.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./redisHelpers.js"), exports);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ZMember, ZRangeOptions } from "@devvit/web/server";
|
|
2
|
+
interface RedisFieldValue {
|
|
3
|
+
field: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scans a sorted set in Redis and returns all members that match a given pattern. This function handles partial results and does multiple zScan calls if needed.
|
|
8
|
+
* @param key The key of the sorted set.
|
|
9
|
+
* @param pattern The pattern to match.
|
|
10
|
+
* @returns {ZMember[]} An array of members that match the pattern.
|
|
11
|
+
*/
|
|
12
|
+
export declare function zScanAll(key: string, pattern: string): Promise<ZMember[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Scans a hash in Redis and returns all fields that match a given pattern. This function handles partial results and does multiple hScan calls if needed.
|
|
15
|
+
* @param key The key of the hash.
|
|
16
|
+
* @param pattern The pattern to match.
|
|
17
|
+
* @returns {Record<string, string>} An object with fields that match the pattern.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hScanAll(key: string, pattern: string): Promise<RedisFieldValue[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Retrieves multiple fields from a Redis hash and returns them as a record/object.
|
|
22
|
+
* @param key The key of the hash.
|
|
23
|
+
* @param fields The fields to retrieve values for
|
|
24
|
+
* @returns {Record<string, string>} An object with field-value pairs for the specified fields.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hMGetAsRecord(key: string, fields: string[]): Promise<Record<string, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* Get all entries from a hash in Redis, chunking the requests if necessary to avoid exceeding Devvit's limitations on response size.
|
|
29
|
+
* @param key The key of the hash.
|
|
30
|
+
* @param chunkSize The size of each chunk.
|
|
31
|
+
* @returns {Record<string, string>} An object with field-value pairs for the specified fields.
|
|
32
|
+
*/
|
|
33
|
+
export declare function hGetAllChunked(key: string, chunkSize?: number): Promise<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* Returns a range of members from a sorted set in Redis as a record/object, where the keys are the members and the values are their scores.
|
|
36
|
+
* @param key The key of the sorted set.
|
|
37
|
+
* @param start The starting index (inclusive) of the range.
|
|
38
|
+
* @param stop The ending index (exclusive) of the range.
|
|
39
|
+
* @param options Optional parameters for the zRange command.
|
|
40
|
+
* @returns {Record<string, number>} A record/object with members and their scores.
|
|
41
|
+
*/
|
|
42
|
+
export declare function zRangeAsRecord(key: string, start: number | string, stop: number | string, options?: ZRangeOptions): Promise<Record<string, number>>;
|
|
43
|
+
/**
|
|
44
|
+
* Sets an expiration time on a Redis key based on a Date object.
|
|
45
|
+
* @param key The redis key.
|
|
46
|
+
* @param expireAt The expiration date.
|
|
47
|
+
*/
|
|
48
|
+
export declare function expireKeyAt(key: string, expireAt: Date): Promise<void>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.zScanAll = zScanAll;
|
|
4
|
+
exports.hScanAll = hScanAll;
|
|
5
|
+
exports.hMGetAsRecord = hMGetAsRecord;
|
|
6
|
+
exports.hGetAllChunked = hGetAllChunked;
|
|
7
|
+
exports.zRangeAsRecord = zRangeAsRecord;
|
|
8
|
+
exports.expireKeyAt = expireKeyAt;
|
|
9
|
+
const server_1 = require("@devvit/web/server");
|
|
10
|
+
const date_fns_1 = require("date-fns");
|
|
11
|
+
const lodash_es_1 = require("lodash-es");
|
|
12
|
+
/**
|
|
13
|
+
* Scans a sorted set in Redis and returns all members that match a given pattern. This function handles partial results and does multiple zScan calls if needed.
|
|
14
|
+
* @param key The key of the sorted set.
|
|
15
|
+
* @param pattern The pattern to match.
|
|
16
|
+
* @returns {ZMember[]} An array of members that match the pattern.
|
|
17
|
+
*/
|
|
18
|
+
async function zScanAll(key, pattern) {
|
|
19
|
+
let cursor = 0;
|
|
20
|
+
const members = {};
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
22
|
+
while (true) {
|
|
23
|
+
const zScanResult = await server_1.redis.zScan(key, cursor, pattern, 1000);
|
|
24
|
+
// Redis scans may return the same member multiple times, so we need to deduplicate them.
|
|
25
|
+
for (const member of zScanResult.members) {
|
|
26
|
+
members[member.member] = member;
|
|
27
|
+
}
|
|
28
|
+
// Return if we reached the end of the scan or if we're stuck in a loop.
|
|
29
|
+
if (zScanResult.cursor === 0 || cursor === zScanResult.cursor) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
cursor = zScanResult.cursor;
|
|
33
|
+
}
|
|
34
|
+
return Object.values(members);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Scans a hash in Redis and returns all fields that match a given pattern. This function handles partial results and does multiple hScan calls if needed.
|
|
38
|
+
* @param key The key of the hash.
|
|
39
|
+
* @param pattern The pattern to match.
|
|
40
|
+
* @returns {Record<string, string>} An object with fields that match the pattern.
|
|
41
|
+
*/
|
|
42
|
+
async function hScanAll(key, pattern) {
|
|
43
|
+
let cursor = 0;
|
|
44
|
+
const fields = {};
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
46
|
+
while (true) {
|
|
47
|
+
const hScanResult = await server_1.redis.hScan(key, cursor, pattern, 1000);
|
|
48
|
+
// Redis scans may return the same field multiple times, so we need to deduplicate them.
|
|
49
|
+
for (const field of hScanResult.fieldValues) {
|
|
50
|
+
fields[field.field] = field;
|
|
51
|
+
}
|
|
52
|
+
// Return if we reached the end of the scan or if we're stuck in a loop.
|
|
53
|
+
if (hScanResult.cursor === 0 || cursor === hScanResult.cursor) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
cursor = hScanResult.cursor;
|
|
57
|
+
}
|
|
58
|
+
return Object.values(fields);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves multiple fields from a Redis hash and returns them as a record/object.
|
|
62
|
+
* @param key The key of the hash.
|
|
63
|
+
* @param fields The fields to retrieve values for
|
|
64
|
+
* @returns {Record<string, string>} An object with field-value pairs for the specified fields.
|
|
65
|
+
*/
|
|
66
|
+
async function hMGetAsRecord(key, fields) {
|
|
67
|
+
if (fields.length === 0) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const results = {};
|
|
71
|
+
const values = await server_1.redis.hMGet(key, fields);
|
|
72
|
+
fields.forEach((field, index) => {
|
|
73
|
+
if (values[index] !== null) {
|
|
74
|
+
results[field] = values[index];
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get all entries from a hash in Redis, chunking the requests if necessary to avoid exceeding Devvit's limitations on response size.
|
|
81
|
+
* @param key The key of the hash.
|
|
82
|
+
* @param chunkSize The size of each chunk.
|
|
83
|
+
* @returns {Record<string, string>} An object with field-value pairs for the specified fields.
|
|
84
|
+
*/
|
|
85
|
+
async function hGetAllChunked(key, chunkSize = 5000) {
|
|
86
|
+
const fields = await server_1.redis.hKeys(key);
|
|
87
|
+
if (fields.length === 0) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
const results = await Promise.all((0, lodash_es_1.chunk)(fields, chunkSize).map(c => hMGetAsRecord(key, c)));
|
|
91
|
+
return Object.assign({}, ...results);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns a range of members from a sorted set in Redis as a record/object, where the keys are the members and the values are their scores.
|
|
95
|
+
* @param key The key of the sorted set.
|
|
96
|
+
* @param start The starting index (inclusive) of the range.
|
|
97
|
+
* @param stop The ending index (exclusive) of the range.
|
|
98
|
+
* @param options Optional parameters for the zRange command.
|
|
99
|
+
* @returns {Record<string, number>} A record/object with members and their scores.
|
|
100
|
+
*/
|
|
101
|
+
async function zRangeAsRecord(key, start, stop, options) {
|
|
102
|
+
const results = {};
|
|
103
|
+
const items = await server_1.redis.zRange(key, start, stop, options);
|
|
104
|
+
items.forEach((item) => {
|
|
105
|
+
results[item.member] = item.score;
|
|
106
|
+
});
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sets an expiration time on a Redis key based on a Date object.
|
|
111
|
+
* @param key The redis key.
|
|
112
|
+
* @param expireAt The expiration date.
|
|
113
|
+
*/
|
|
114
|
+
async function expireKeyAt(key, expireAt) {
|
|
115
|
+
const secondsUntil = (0, date_fns_1.differenceInSeconds)(expireAt, new Date());
|
|
116
|
+
if (secondsUntil > 0) {
|
|
117
|
+
await server_1.redis.expire(key, secondsUntil);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw new Error(`Expire time ${expireAt.toISOString()} is in the past`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./upgradeNotifier.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./upgradeNotifier.js"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
export type UpgradeNotifierData = {
|
|
3
|
+
settingName?: string;
|
|
4
|
+
appFriendlyName?: string;
|
|
5
|
+
updateSubreddit?: string;
|
|
6
|
+
updateWikiPage?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const handleUpgradeNotifier: (c: Context) => Promise<Response & import("hono").TypedResponse<{}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleUpgradeNotifier = void 0;
|
|
7
|
+
const server_1 = require("@devvit/web/server");
|
|
8
|
+
const json2md_1 = __importDefault(require("json2md"));
|
|
9
|
+
const semver_1 = require("semver");
|
|
10
|
+
const DEFAULT_UPDATE_SUBREDDIT = "fsvapps";
|
|
11
|
+
const DEFAULT_UPDATE_WIKI_PAGE = "upgrade-notifier";
|
|
12
|
+
const handleUpgradeNotifier = async (c) => {
|
|
13
|
+
const request = await c.req.json();
|
|
14
|
+
if (!request.data) {
|
|
15
|
+
console.error("Update Checker: No data provided in request");
|
|
16
|
+
return c.json({ message: "upgrade notifier: no data provided" }, 400);
|
|
17
|
+
}
|
|
18
|
+
if (request.data.settingName) {
|
|
19
|
+
const checkForUpdates = await server_1.settings.get(request.data.settingName);
|
|
20
|
+
if (!checkForUpdates) {
|
|
21
|
+
console.log("Update Checker: Upgrade notifier is disabled");
|
|
22
|
+
return c.json({ message: "upgrade notifier: upgrade notifier is disabled" }, 200);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const updateSubreddit = request.data.updateSubreddit ?? DEFAULT_UPDATE_SUBREDDIT;
|
|
26
|
+
const updateWikiPage = request.data.updateWikiPage ?? DEFAULT_UPDATE_WIKI_PAGE;
|
|
27
|
+
let wikiPage;
|
|
28
|
+
try {
|
|
29
|
+
wikiPage = await server_1.reddit.getWikiPage(updateSubreddit, updateWikiPage);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
console.error(`Update Checker: Error getting wiki page ${updateWikiPage} from ${updateSubreddit}`);
|
|
33
|
+
return c.json({ message: "upgrade notifier: could not retrieve wiki page" }, 500);
|
|
34
|
+
}
|
|
35
|
+
const updates = JSON.parse(wikiPage.content);
|
|
36
|
+
const updatesForThisApp = updates.filter(update => update.appname === server_1.context.appSlug);
|
|
37
|
+
if (updatesForThisApp.length === 0) {
|
|
38
|
+
console.log(`Update Checker: No updates found for app ${server_1.context.appSlug}`);
|
|
39
|
+
return c.json({ message: "upgrade notifier: no updates found" }, 200);
|
|
40
|
+
}
|
|
41
|
+
if (updatesForThisApp.length > 1) {
|
|
42
|
+
console.error(`Update Checker: Multiple updates found for app ${server_1.context.appSlug}`);
|
|
43
|
+
return c.json({ message: "upgrade notifier: multiple updates found" }, 500);
|
|
44
|
+
}
|
|
45
|
+
const update = updatesForThisApp[0];
|
|
46
|
+
let newVersionAvailable;
|
|
47
|
+
try {
|
|
48
|
+
newVersionAvailable = (0, semver_1.lt)(server_1.context.appVersion, update.version);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
52
|
+
console.error(`Update Checker: Error comparing versions: ${message}`);
|
|
53
|
+
return c.json({ message: "upgrade notifier: error comparing versions" }, 500);
|
|
54
|
+
}
|
|
55
|
+
if (!newVersionAvailable) {
|
|
56
|
+
console.log("Update Checker: No relevant updates found");
|
|
57
|
+
return c.json({ message: "upgrade notifier: no relevant updates found" }, 200);
|
|
58
|
+
}
|
|
59
|
+
const redisKey = "fdwh:update-notification-sent";
|
|
60
|
+
const notificationSent = await server_1.redis.get(redisKey);
|
|
61
|
+
if (notificationSent === update.version) {
|
|
62
|
+
console.log("Update Checker: Update notification already sent");
|
|
63
|
+
return c.json({ message: "upgrade notifier: update notification already sent" }, 200);
|
|
64
|
+
}
|
|
65
|
+
const message = [
|
|
66
|
+
{ p: `A new version of ${request.data.appFriendlyName} is available to install.` },
|
|
67
|
+
];
|
|
68
|
+
if (update.whatsNewBullets.length > 0) {
|
|
69
|
+
message.push({ p: "Here's what's new:" });
|
|
70
|
+
message.push({ ul: update.whatsNewBullets });
|
|
71
|
+
}
|
|
72
|
+
message.push({ p: `To install this update, or to disable these notifications, visit the [**${request.data.appFriendlyName ?? "App"} configuration page**](https://developers.reddit.com/r/${server_1.context.subredditName}/apps/${server_1.context.appSlug}) for /r/${server_1.context.subredditName}.` });
|
|
73
|
+
await server_1.reddit.modMail.createModNotification({
|
|
74
|
+
subredditId: server_1.context.subredditId,
|
|
75
|
+
subject: `New update available for ${request.data.appFriendlyName ?? server_1.context.appSlug}: v${update.version}`,
|
|
76
|
+
bodyMarkdown: (0, json2md_1.default)(message),
|
|
77
|
+
});
|
|
78
|
+
return c.json({ message: "upgrade notifier handled" }, 200);
|
|
79
|
+
};
|
|
80
|
+
exports.handleUpgradeNotifier = handleUpgradeNotifier;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasTriggerBeenHandled = hasTriggerBeenHandled;
|
|
4
|
+
const server_1 = require("@devvit/web/server");
|
|
5
|
+
const date_fns_1 = require("date-fns");
|
|
6
|
+
const redis_1 = require("../redis");
|
|
7
|
+
async function hasTriggerBeenHandled(identifier, opts) {
|
|
8
|
+
const redisKey = `triggerLock:${identifier}`;
|
|
9
|
+
const expiration = opts?.expiration ?? (0, date_fns_1.addDays)(new Date(), 1);
|
|
10
|
+
const newVal = await server_1.redis.incrBy(redisKey, 1);
|
|
11
|
+
await (0, redis_1.expireKeyAt)(redisKey, expiration);
|
|
12
|
+
return newVal > 1;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./duplicateTriggerMitigation.js"), exports);
|
|
18
|
+
__exportStar(require("./postsAndComments.js"), exports);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { OnCommentCreateRequest, OnCommentReportRequest, OnCommentSubmitRequest, OnCommentUpdateRequest, OnPostCreateRequest, OnPostFlairUpdateRequest, OnPostReportRequest, OnPostSubmitRequest, OnPostUpdateRequest } from "@devvit/web/shared";
|
|
2
|
+
export declare function fixPostTriggerEvent<T extends OnPostSubmitRequest | OnPostCreateRequest | OnPostUpdateRequest | OnPostFlairUpdateRequest>(event: T): Promise<T>;
|
|
3
|
+
export declare function fixCommentTriggerEvent<T extends OnCommentSubmitRequest | OnCommentCreateRequest | OnCommentUpdateRequest>(event: T): Promise<T>;
|
|
4
|
+
export declare function fixCommentReportTriggerEvent(event: OnCommentReportRequest): Promise<OnCommentReportRequest>;
|
|
5
|
+
export declare function fixPostReportTriggerEvent(event: OnPostReportRequest): Promise<OnPostReportRequest>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fixPostTriggerEvent = fixPostTriggerEvent;
|
|
4
|
+
exports.fixCommentTriggerEvent = fixCommentTriggerEvent;
|
|
5
|
+
exports.fixCommentReportTriggerEvent = fixCommentReportTriggerEvent;
|
|
6
|
+
exports.fixPostReportTriggerEvent = fixPostReportTriggerEvent;
|
|
7
|
+
const server_1 = require("@devvit/web/server");
|
|
8
|
+
async function fixPostTriggerEvent(event) {
|
|
9
|
+
const eventToReturn = { ...event };
|
|
10
|
+
if (!eventToReturn.post || !eventToReturn.author) {
|
|
11
|
+
return eventToReturn;
|
|
12
|
+
}
|
|
13
|
+
if (eventToReturn.author.name !== "[redacted]" && eventToReturn.post.selftext !== "[Removed by Reddit]") {
|
|
14
|
+
return eventToReturn;
|
|
15
|
+
}
|
|
16
|
+
const post = await server_1.reddit.getPostById(eventToReturn.post.id);
|
|
17
|
+
if (post.body !== undefined) {
|
|
18
|
+
eventToReturn.post.selftext = post.body;
|
|
19
|
+
}
|
|
20
|
+
eventToReturn.author.name = post.authorName;
|
|
21
|
+
if (post.authorId) {
|
|
22
|
+
eventToReturn.author.id = post.authorId;
|
|
23
|
+
}
|
|
24
|
+
return eventToReturn;
|
|
25
|
+
}
|
|
26
|
+
async function fixCommentTriggerEvent(event) {
|
|
27
|
+
const eventToReturn = { ...event };
|
|
28
|
+
if (!eventToReturn.comment || !eventToReturn.author) {
|
|
29
|
+
return eventToReturn;
|
|
30
|
+
}
|
|
31
|
+
if (eventToReturn.author.name !== "[redacted]" && eventToReturn.comment.body !== "[Removed by Reddit]") {
|
|
32
|
+
return eventToReturn;
|
|
33
|
+
}
|
|
34
|
+
const comment = await server_1.reddit.getCommentById(eventToReturn.comment.id);
|
|
35
|
+
eventToReturn.comment.body = comment.body;
|
|
36
|
+
eventToReturn.author.name = comment.authorName;
|
|
37
|
+
if (comment.authorId) {
|
|
38
|
+
eventToReturn.author.id = comment.authorId;
|
|
39
|
+
}
|
|
40
|
+
return eventToReturn;
|
|
41
|
+
}
|
|
42
|
+
async function fixCommentReportTriggerEvent(event) {
|
|
43
|
+
const eventToReturn = { ...event };
|
|
44
|
+
if (!eventToReturn.comment) {
|
|
45
|
+
return eventToReturn;
|
|
46
|
+
}
|
|
47
|
+
if (eventToReturn.comment.body !== "[Removed by Reddit]") {
|
|
48
|
+
return eventToReturn;
|
|
49
|
+
}
|
|
50
|
+
const comment = await server_1.reddit.getCommentById(eventToReturn.comment.id);
|
|
51
|
+
eventToReturn.comment.body = comment.body;
|
|
52
|
+
return eventToReturn;
|
|
53
|
+
}
|
|
54
|
+
async function fixPostReportTriggerEvent(event) {
|
|
55
|
+
const eventToReturn = { ...event };
|
|
56
|
+
if (!eventToReturn.post) {
|
|
57
|
+
return eventToReturn;
|
|
58
|
+
}
|
|
59
|
+
if (eventToReturn.post.selftext !== "[Removed by Reddit]") {
|
|
60
|
+
return eventToReturn;
|
|
61
|
+
}
|
|
62
|
+
const post = await server_1.reddit.getPostById(eventToReturn.post.id);
|
|
63
|
+
if (post.body !== undefined) {
|
|
64
|
+
eventToReturn.post.selftext = post.body;
|
|
65
|
+
}
|
|
66
|
+
return eventToReturn;
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fsvreddit/fsv-devvit-web-helpers",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Helper functions and utilities for Devvit apps on Reddit",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "npm run test:unit",
|
|
8
|
+
"test:unit": "vitest --run",
|
|
9
|
+
"lint": "eslint . --ext .ts",
|
|
10
|
+
"build": "tsc --project tsconfig.build.json",
|
|
11
|
+
"prepare": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/fsvreddit/fsv-devvit-web-helpers.git"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"author": "fsv",
|
|
21
|
+
"license": "BSD-3-Clause",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"date-fns": "^4.1.0",
|
|
24
|
+
"hono": "^4.12.30",
|
|
25
|
+
"json2md": "^2.0.3",
|
|
26
|
+
"lodash-es": "^4.18.1",
|
|
27
|
+
"marked": "^18.0.11",
|
|
28
|
+
"semver": "^7.8.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@fsvreddit/eslint-config": "^0.1.0",
|
|
32
|
+
"@types/json2md": "^1.5.4",
|
|
33
|
+
"@types/lodash-es": "^4.17.12",
|
|
34
|
+
"@types/semver": "^7.7.1",
|
|
35
|
+
"eslint": "^10.0.2",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^4.0.15"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"reddit",
|
|
41
|
+
"devvit"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@devvit/web": "^0.14.1"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022",
|
|
6
|
+
"WebWorker"
|
|
7
|
+
],
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"module": "NodeNext",
|
|
14
|
+
"moduleResolution": "NodeNext",
|
|
15
|
+
},
|
|
16
|
+
"exclude": [
|
|
17
|
+
"**/node_modules",
|
|
18
|
+
"dist",
|
|
19
|
+
"**/.*/",
|
|
20
|
+
"eslint.config.mjs"
|
|
21
|
+
]
|
|
22
|
+
}
|