Compare commits

...

10 Commits

Author SHA1 Message Date
sloppyjosh 0b91c0dcf5 removed unneeded import 2024-03-08 02:02:57 -05:00
sloppyjosh f2b4d367a4 Message Service to static Method Documentation 2024-03-08 01:58:05 -05:00
sloppyjosh f80ef1005d Minor Formatting, Commented all methods 2024-03-07 02:04:34 -05:00
sloppyjosh 28e280556e updated database service to static method 2024-03-07 01:52:44 -05:00
sloppyjosh 1603c7bf07 thoroughly testing notifications and timing 2024-03-06 00:17:42 -05:00
sloppyjosh 35956bb7b1 notification logic 2024-03-05 01:25:05 -05:00
sloppyjosh faa74287bb Cleanup 2024-03-05 01:24:39 -05:00
sloppyjosh af5b35589d reddit service implementation 2024-03-04 00:28:20 -05:00
sloppyjosh 522ec2de51 reddit session handling 2024-03-02 02:41:11 -05:00
sloppyjosh a1d5f75d3c Comment Posting Functionality 2024-03-01 01:40:34 -05:00
20 changed files with 988 additions and 124 deletions

314
package-lock.json generated
View File

@ -9,8 +9,10 @@
"axios-request-throttle": "^1.0.0",
"axios-retry": "^4.0.0",
"dotenv": "^16.4.5",
"form-data": "^4.0.0",
"fs": "^0.0.1-security",
"path": "^0.12.7",
"qs": "^6.11.2",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
@ -306,6 +308,24 @@
"node": ">= 10"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -448,6 +468,22 @@
"node": ">=4.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -519,6 +555,25 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"optional": true
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@ -600,6 +655,14 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
@ -628,6 +691,24 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@ -653,18 +734,73 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"optional": true
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"optional": true
},
"node_modules/hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -1152,6 +1288,14 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1270,6 +1414,20 @@
"once": "^1.3.1"
}
},
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@ -1375,6 +1533,39 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"optional": true
},
"node_modules/set-function-length": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
"integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
"dependencies": {
"define-data-property": "^1.1.2",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.3",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@ -2044,6 +2235,18 @@
"unique-filename": "^1.1.1"
}
},
"call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"requires": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
}
},
"chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -2150,6 +2353,16 @@
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"requires": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -2206,6 +2419,19 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"optional": true
},
"es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"requires": {
"get-intrinsic": "^1.2.4"
}
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@ -2261,6 +2487,11 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"gauge": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
@ -2283,6 +2514,18 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
}
},
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@ -2302,18 +2545,52 @@
"path-is-absolute": "^1.0.0"
}
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"requires": {
"get-intrinsic": "^1.1.3"
}
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"optional": true
},
"has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"requires": {
"es-define-property": "^1.0.0"
}
},
"has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"optional": true
},
"hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"requires": {
"function-bind": "^1.1.2"
}
},
"http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -2685,6 +2962,11 @@
"set-blocking": "^2.0.0"
}
},
"object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -2782,6 +3064,14 @@
"once": "^1.3.1"
}
},
"qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"requires": {
"side-channel": "^1.0.4"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@ -2849,6 +3139,30 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"optional": true
},
"set-function-length": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
"integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
"requires": {
"define-data-property": "^1.1.2",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.3",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
}
},
"side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"requires": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
}
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",

View File

@ -4,8 +4,10 @@
"axios-request-throttle": "^1.0.0",
"axios-retry": "^4.0.0",
"dotenv": "^16.4.5",
"form-data": "^4.0.0",
"fs": "^0.0.1-security",
"path": "^0.12.7",
"qs": "^6.11.2",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
@ -15,7 +17,7 @@
"typescript": "^5.3.3"
},
"scripts": {
"build": "tsc && copyfiles -u 3 \"src/db/migrations/*.sql\" dist/db/migrations && copyfiles -u 3 \"src/db/seed/*.sql\" dist/db/seed",
"build": "tsc && copyfiles -u 3 \"src/db/migrations/*.sql\" dist/db/migrations && copyfiles -u 3 \"src/db/seed/*.sql\" dist/db/seed && copyfiles -u 2 \"src/messages/*.txt\" dist/messages",
"start": "node dist/index.js"
}
}
}

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS oauth_tokens (
id INTEGER PRIMARY KEY,
token_identifier TEXT NOT NULL UNIQUE, -- Static identifier for the OAuth token
access_token TEXT NOT NULL,
token_type TEXT NOT NULL,
expires_in INTEGER NOT NULL,
expiry_timestamp INTEGER NOT NULL,
scope TEXT NOT NULL
);

View File

@ -1,30 +1,50 @@
import { Database } from 'sqlite';
import { Comment } from '../../rdrama/models/Comment';
import { DatabaseInitializer } from '../initializeDatabase';
/**
* Service for interacting with the SQLite database for operations related to comments and user mentions.
*/
export class DatabaseService {
private db: Database;
/**
* Creates a new DatabaseService instance with a provided database connection.
* Retrieves the singleton instance of the database.
* This static method ensures that a single database instance is used throughout the application,
* following the singleton pattern for managing database connections.
*
* @param {Database} db - The SQLite database connection.
* @example
* const db = await DatabaseService.getDatabase();
*
* @returns {Promise<Database>} A promise that resolves to the initialized database instance.
* @throws {Error} Will throw an error if the database cannot be initialized.
*/
public constructor(db: Database) {
this.db = db;
private static async getDatabase(): Promise<Database> {
const databaseInitializer = DatabaseInitializer.getInstance();
const db = await databaseInitializer.getDbInstance()
if (!db) {
throw new Error('Failed to initialize the database.');
}
return db
}
/**
* Inserts a new comment into the database.
*
* This method constructs an SQL statement to insert all fields of the Comment object
* This static method constructs an SQL statement to insert all fields of the Comment object
* into the corresponding columns in the 'comments' table.
*
* @example
* await DatabaseService.insertComment({
* id: 1,
* author_id: 123,
* author_name: 'exampleUser',
* body: 'This is a comment.',
* // More fields as per the Comment type
* });
*
* @param {Comment} comment - The comment object to insert.
* @throws {Error} Will throw an error if the insert operation fails.
*/
public async insertComment(comment: Comment): Promise<void> {
public static async insertComment(comment: Comment): Promise<void> {
const db = await DatabaseService.getDatabase()
const sql = `
INSERT INTO comments (
id, author_id, author_name, body, body_html, created_utc, deleted_utc,
@ -36,7 +56,7 @@ export class DatabaseService {
?, ?, ?, ?, ?, ?, ?
)
`;
await this.db.run(sql, [
await db.run(sql, [
comment.id, comment.author_id, comment.author_name, comment.body, comment.body_html, comment.created_utc, comment.deleted_utc,
comment.distinguished ? 1 : 0, comment.downvotes, comment.edited_utc, comment.is_banned ? 1 : 0, comment.is_bot ? 1 : 0, comment.is_nsfw ? 1 : 0, comment.level,
comment.permalink, comment.pinned, comment.post_id, JSON.stringify(comment.replies), JSON.stringify(comment.reports), comment.score, comment.upvotes
@ -45,62 +65,91 @@ export class DatabaseService {
/**
* Inserts a new user mention into the database.
* This static method adds a record of a user being mentioned in a comment.
*
* @param {Object} mention - The user mention object to insert, containing rdrama_comment_id, username, and optionally message.
* @example
* await DatabaseService.insertUserMention({
* rdrama_comment_id: 456,
* username: 'mentionedUser',
* message: 'You were mentioned in a comment.'
* });
*
* @param {Object} mention - The user mention object to insert.
* @param {number} mention.rdrama_comment_id - The ID of the comment from the r/Drama platform.
* @param {string} mention.username - The mentioned Reddit username.
* @param {string} [mention.message] - The content of the message sent to the mentioned user.
* @throws {Error} Will throw an error if the insert operation fails.
*/
public async insertUserMention(mention: { rdrama_comment_id: string; username: string; message?: string }): Promise<void> {
public static async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise<void> {
const db = await DatabaseService.getDatabase()
const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`;
await this.db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
await db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
}
/**
* Queries the database for an existing comment.
* Queries the database for an existing comment by its ID.
*
* @example
* const exists = await DatabaseService.commentExists('123');
* console.log(exists ? 'Comment exists.' : 'Comment does not exist.');
*
* @param {string} commentId - The ID of the comment to search for.
* @returns {Promise<boolean>} A boolean indicating whether the comment exists.
* @throws {Error} Will throw an error if the query operation fails.
*/
public async commentExists(commentId: string): Promise<boolean> {
public static async commentExists(commentId: string): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const sql = `SELECT 1 FROM comments WHERE id = ?`;
const result = await this.db.get(sql, [commentId]);
const result = await db.get(sql, [commentId]);
return !!result;
}
/**
* Queries the database for existing mentions of a username.
* Queries the database to check if a username has been mentioned.
*
* @example
* const mentioned = await DatabaseService.userMentionExists('exampleUser');
* console.log(mentioned ? 'User has been mentioned.' : 'User has not been mentioned.');
*
* @param {string} username - The username to search for.
* @returns {Promise<boolean>} A boolean indicating whether the username has been mentioned.
* @throws {Error} Will throw an error if the query operation fails.
*/
public async userMentionExists(username: string): Promise<boolean> {
public static async userMentionExists(username: string): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const sql = `SELECT 1 FROM user_mentions WHERE username = ?`;
const result = await this.db.get(sql, [username]);
const result = await db.get(sql, [username]);
return !!result;
}
/**
* Retrieves the last run timestamp for a specified maintenance task.
* This method queries the `maintenance_log` table to find the last time a specific task was run.
* Updates the last run timestamp for a maintenance task, using an "upsert" approach.
*
* @param {string} taskName - The name of the maintenance task for which to retrieve the last run timestamp.
* @returns {Promise<Date | null>} A promise that resolves to the Date of the last run if found, or null if not found.
* @example
* await DatabaseService.updateLastRunTimestamp('purgeOldComments');
*
* @param {string} taskName - The name of the maintenance task.
* @throws {Error} Will throw an error if the update operation fails.
*/
public async getLastRunTimestamp(taskName: string): Promise<Date | null> {
const result = await this.db.get(`SELECT last_run FROM maintenance_log WHERE task_name = ?`, [taskName]);
public static async getLastRunTimestamp(taskName: string): Promise<Date | null> {
const db = await DatabaseService.getDatabase()
const result = await db.get(`SELECT last_run FROM maintenance_log WHERE task_name = ?`, [taskName]);
return result ? new Date(result.last_run) : null;
}
/**
* Updates the last run timestamp for a specific maintenance task in the `maintenance_log` table.
* This method uses an "upsert" approach, which inserts a new record if one doesn't exist for the task,
* or updates the existing record if it does. This ensures that each task has only one record in the table
* reflecting its most recent execution time.
* Updates the last run timestamp for a maintenance task, using an "upsert" approach.
*
* @param {string} taskName - The name of the maintenance task for which to update the last run timestamp.
* @returns {Promise<void>} A promise that resolves when the operation is complete.
* @example
* await DatabaseService.updateLastRunTimestamp('purgeOldComments');
*
* @param {string} taskName - The name of the maintenance task.
* @throws {Error} Will throw an error if the update operation fails.
*/
public async updateLastRunTimestamp(taskName: string): Promise<void> {
public static async updateLastRunTimestamp(taskName: string): Promise<void> {
// Assumes an "upsert" approach for the maintenance_log table
await this.db.run(
const db = await DatabaseService.getDatabase()
await db.run(
`INSERT INTO maintenance_log (task_name, last_run)
VALUES (?, ?)
ON CONFLICT(task_name)
@ -110,16 +159,113 @@ export class DatabaseService {
}
/**
* Deletes comments from the database that are older than a specified number of days.
* This method is intended to be run as part of periodic maintenance tasks to keep the database size manageable.
* Deletes comments from the database older than a specified number of days.
*
* @param {number} [days=1] - The age of comments to be purged, in days. Defaults to 30 days.
* @example
* await DatabaseService.purgeOldComments(30); // Purge comments older than 30 days
*
* @param {number} days - The age of comments to be purged, in days.
* @throws {Error} Will throw an error if the purge operation fails.
*/
public async purgeOldComments(days: number = 1): Promise<void> {
public static async purgeOldComments(days: number = 1): Promise<void> {
const db = await DatabaseService.getDatabase()
console.log(`Purging comments older than ${days} days...`);
await this.db.run(`
await db.run(`
DELETE FROM comments
WHERE datetime(created_utc, 'unixepoch') < datetime('now', '-${days} days')
`);
}
/**
* Inserts or updates the OAuth token in the database for a specific service.
*
* @example
* await DatabaseService.upsertOAuthToken('https://oauth.reddit.com', {
* access_token: 'abc123',
* token_type: 'bearer',
* expires_in: 3600,
* scope: 'read'
* });
*
* @param {string} token_identifier - A unique identifier for the token, typically the service's base URL.
* @param {Object} tokenData - The OAuth token data including access_token, token_type, expires_in, and scope.
* @throws {Error} Will throw an error if the upsert operation fails.
*/
public static async upsertOAuthToken(token_identifier: string, tokenData: any) {
const db = await DatabaseService.getDatabase()
const { access_token, token_type, expires_in, scope } = tokenData;
const expiryTimestamp = Math.floor(Date.now() / 1000) + expires_in;
console.log('token_identifier', token_identifier)
console.log('access_token', `${access_token.substring(0, 5)}XXXXX`)
console.log('token_type', token_type)
console.log('expires_in', expires_in)
console.log('scope', scope)
await db.run(`
INSERT INTO oauth_tokens (token_identifier, access_token, token_type, expires_in, expiry_timestamp, scope)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(token_identifier) DO UPDATE SET
access_token = excluded.access_token,
token_type = excluded.token_type,
expires_in = excluded.expires_in,
expiry_timestamp = excluded.expiry_timestamp,
scope = excluded.scope
`, [token_identifier, access_token, token_type, expires_in, expiryTimestamp, scope]);
}
/**
* Retrieves the current, unexpired OAuth token for a specific service.
*
* @example
* const token = await DatabaseService.getCurrentOAuthToken('https://oauth.reddit.com');
* console.log(token ? `Current token: ${token.access_token}` : 'No valid token found.');
*
* @param {string} token_identifier - The unique identifier for the token, typically the service's base URL.
* @returns {Promise<Object|null>} The current OAuth token data or null if expired or not found.
* @throws {Error} Will throw an error if the query operation fails.
*/
public static async getCurrentOAuthToken(token_identifier: string) {
const db = await DatabaseService.getDatabase()
const tokenRow = await db.get(`
SELECT access_token, token_type, scope, expiry_timestamp FROM oauth_tokens
WHERE token_identifier = ?
`, token_identifier);
return tokenRow || null;
}
/**
* Checks if the cooldown period has passed since the last notification was sent, allowing for a new notification to be sent.
*
* @example
* const canSend = await DatabaseService.canSendNotification();
* console.log(canSend ? 'Can send a new notification.' : 'Still in cooldown period.');
*
* @returns {Promise<boolean>} True if the cooldown period has passed, allowing new notifications to be sent.
* @throws {Error} Will throw an error if the check operation fails.
*/
public static async canSendNotification(): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const cooldownHours = process.env.NOTIFICATION_COOLDOWN_HOURS || 4;
const sql = `
SELECT MAX(sent_time) as last_notification_time
FROM user_mentions
`;
const result = await db.get(sql);
if (!result || !result.last_notification_time) {
// No notifications have been sent yet, or unable to retrieve the last sent time.
return true;
}
const lastNotificationTime = new Date(result.last_notification_time).getTime();
const currentTime = new Date(new Date().toISOString().slice(0, 19).replace('T', ' ')).getTime();
const timeElapsed = currentTime - lastNotificationTime;
console.log('timeElapsed', timeElapsed)
const cooldownPeriod = +cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
console.log('cooldownPeriod', cooldownPeriod)
return timeElapsed >= cooldownPeriod;
}
}

View File

@ -5,15 +5,7 @@ import { DatabaseService } from './Database';
* This service is responsible for periodically running maintenance tasks based on specified intervals.
*/
export class DatabaseMaintenanceService {
private databaseService: DatabaseService;
/**
* Initializes a new instance of the DatabaseMaintenanceService.
* @param {DatabaseService} databaseService - An instance of DatabaseService for database operations.
*/
constructor(databaseService: DatabaseService) {
this.databaseService = databaseService;
}
/**
* A list of maintenance tasks to be executed, each with a name, action, and interval.
@ -49,7 +41,7 @@ export class DatabaseMaintenanceService {
*/
private async shouldRunTask(taskName: string, interval: number): Promise<boolean> {
// Use the DatabaseService to check the last run timestamp from the maintenance_log table
const lastRun = await this.databaseService.getLastRunTimestamp(taskName);
const lastRun = await DatabaseService.getLastRunTimestamp(taskName);
if (!lastRun) return true; // Task has never run
const now = Date.now();
@ -62,7 +54,7 @@ export class DatabaseMaintenanceService {
private async purgeOldComments() {
console.log("Purging old comments...");
// Use the DatabaseService for the SQL operation
await this.databaseService.purgeOldComments();
await DatabaseService.purgeOldComments();
}
/**
@ -72,6 +64,6 @@ export class DatabaseMaintenanceService {
*/
private async updateLastRunTimestamp(taskName: string) {
// Use the DatabaseService to update the last run timestamp in the maintenance_log table
await this.databaseService.updateLastRunTimestamp(taskName);
await DatabaseService.updateLastRunTimestamp(taskName);
}
}

View File

@ -2,40 +2,55 @@ import dotenv from 'dotenv';
dotenv.config();
import WorkflowOrchestrator from './workflows/WorkflowOrchestrator';
import SessionManager from './rdrama/session/SessionManager';
import rDramaSession from './rdrama/session/SessionManager';
import redditSession from './reddit/session/SessionManager';
import { CommentParser } from './rdrama/services/CommentParser';
import { DatabaseInitializer } from './db/initializeDatabase';
import { DatabaseService } from './db/services/Database';
import { DatabaseMaintenanceService } from './db/services/DatabaseMaintenance';
import { CommentProcessor } from './rdrama/services/CommentProcessor';
import { CommentPoster } from './rdrama/services/CommentPoster';
// Import other necessary services or configurations
async function startApplication() {
// Initialize SessionManager or other global configurations
const sessionManager = SessionManager.getInstance();
if (!process.env.RDRAMA_API_KEY) {
throw new Error('RDRAMA_API_KEY is undefined. Please set this environment variable.');
}
sessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY);
console.log('Database Start')
const databaseInitializer = DatabaseInitializer.getInstance();
const db = await databaseInitializer.getDbInstance()
if (!db) {
throw new Error('Failed to initialize the database.');
}
const databaseService = new DatabaseService(db)
const databaseMaintenance = new DatabaseMaintenanceService(databaseService)
const canSend = await DatabaseService.canSendNotification();
const coolDownHours = process.env.NOTIFICATION_COOLDOWN_HOURS
if (!canSend) {
console.log(`Last Message Sent less than ${coolDownHours ? coolDownHours : 4} hours ago. Set NOTIFICATION_COOLDOWN_HOURS to change this`)
return;
}
console.log('RDrama Session Start')
// Initialize SessionManager or other global configurations
const rDramaSessionManager = rDramaSession.getInstance();
if (!process.env.RDRAMA_API_KEY) {
throw new Error('RDRAMA_API_KEY is undefined. Please set this environment variable.');
}
rDramaSessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY);
console.log('Database Maintenance Start')
const databaseMaintenance = new DatabaseMaintenanceService()
await databaseMaintenance.runMaintenanceTasks()
console.log('Reddit Session Start')
await redditSession.getInstance()
// Initialize services with any required dependencies
const commentFetcher = new CommentProcessor(databaseService);
const commentFetcher = new CommentProcessor();
const commentParser = new CommentParser();
const commentPoster = new CommentPoster()
// Initialize and start your workflow
const workflowOrchestrator = new WorkflowOrchestrator(
commentFetcher,
commentParser
commentParser,
commentPoster
);
await workflowOrchestrator.executeWorkflow();
}

View File

@ -10,20 +10,20 @@ export class CommentParser {
*/
public extractUsernames(comment: Comment): string[] {
const foundUsernames: Set<string> = new Set();
const matches = comment.body.match(this.regexPattern);
if (matches) {
matches.forEach(match => {
// Ensure the username is captured in a standardized format
const usernameMatch = match.trim().match(/\/?u\/([a-zA-Z0-9_]+)/);
if (usernameMatch) {
// Standardize to "u/username" format
const username = `u/${usernameMatch[1].toLowerCase()}`;
// Standardize to "username" format
const username = `${usernameMatch[1].toLowerCase()}`;
foundUsernames.add(username);
}
});
}
return Array.from(foundUsernames);
}
}

View File

@ -0,0 +1,37 @@
import SessionManager from '../session/SessionManager';
import { Comment } from '../models/Comment';
import FormData from 'form-data';
export class CommentPoster {
private sessionManager: SessionManager;
constructor() {
this.sessionManager = SessionManager.getInstance();
}
/**
* Posts a comment as a reply to a given rdrama comment.
*
* @param parentId The ID of the parent comment to reply to. Expected format: 'c_{id}'.
* @param body The body of the comment to post.
* @returns A promise resolving to the Axios response.
*/
public async postComment(parentId: string, body: string): Promise<Comment> {
const formData = new FormData();
formData.append('parent_fullname', parentId);
formData.append('body', body);
try {
const response = await this.sessionManager.axiosInstance.post('/comment', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log(`Comment posted successfully to ${parentId}`);
return response.data;
} catch (error) {
console.error(`Failed to post comment to ${parentId}:`, error);
throw error; // Rethrow for handling elsewhere
}
}
}

View File

@ -9,15 +9,13 @@ import { CommentFetcher } from './CommentFetcher';
*/
export class CommentProcessor {
private maxPages: number;
private databaseService: DatabaseService;
/**
* Creates an instance of CommentProcessor.
* @param {DatabaseService} databaseService - The service for database operations.
* @param {number} maxPages - The maximum number of pages to fetch from the r/Drama API. Defaults to 10.
*/
constructor(databaseService: DatabaseService, maxPages: number = 10) {
this.databaseService = databaseService;
constructor(maxPages: number = 10) {
this.maxPages = maxPages;
}
@ -41,12 +39,12 @@ export class CommentProcessor {
// Check if the comment was already processed in this batch
if (comments.some(c => c.id === comment.id)) continue;
const exists = await this.databaseService.commentExists(comment.id.toString());
const exists = await DatabaseService.commentExists(comment.id.toString());
if (exists) {
stopFetching = true;
break; // Stop processing this batch of comments
}
await this.databaseService.insertComment(comment)
await DatabaseService.insertComment(comment)
comments.push(comment);
}

View File

@ -0,0 +1,44 @@
export type Subreddit = {
default_set?: boolean;
user_is_contributor?: boolean;
banner_img?: string;
allowed_media_in_comments?: any[];
user_is_banned?: boolean;
free_form_reports?: boolean;
community_icon?: string | null;
show_media?: boolean;
icon_color?: string;
user_is_muted?: boolean | null;
display_name?: string;
header_img?: string | null;
title?: string;
coins?: number;
previous_names?: any[];
over_18?: boolean;
icon_size?: number[] | null;
primary_color?: string;
icon_img?: string;
description?: string;
submit_link_label?: string;
header_size?: number[] | null;
restrict_posting?: boolean;
restrict_commenting?: boolean;
subscribers?: number;
submit_text_label?: string;
is_default_icon?: boolean;
link_flair_position?: string;
display_name_prefixed?: string;
key_color?: string;
name?: string;
is_default_banner?: boolean;
url?: string;
quarantine?: boolean;
banner_size?: number[] | null;
user_is_moderator?: boolean;
accept_followers?: boolean;
public_description?: string;
link_flair_enabled?: boolean;
disable_contributor_requests?: boolean;
subreddit_type?: string;
user_is_subscriber?: boolean;
};

View File

@ -0,0 +1,71 @@
import { Subreddit } from "./Subreddit";
export type RedditUser = {
kind: string;
data: {
is_employee?: boolean;
has_visited_new_profile?: boolean;
is_friend: boolean;
pref_no_profanity?: boolean;
has_external_account?: boolean;
pref_geopopular?: string;
pref_show_trending?: boolean;
subreddit: Subreddit
pref_show_presence?: boolean;
snoovatar_img?: string;
snoovatar_size?: number[] | null;
gold_expiration?: null;
has_gold_subscription?: boolean;
is_sponsor?: boolean;
num_friends?: number;
features?: any;
can_edit_name?: boolean;
is_blocked?: boolean;
verified?: boolean;
new_modmail_exists?: null;
pref_autoplay?: boolean;
coins?: number;
has_paypal_subscription?: boolean;
has_subscribed_to_premium?: boolean;
id: string;
can_create_subreddit?: boolean;
over_18?: boolean;
is_gold?: boolean;
is_mod?: boolean;
awarder_karma?: number;
suspension_expiration_utc?: null;
has_stripe_subscription?: boolean;
is_suspended?: boolean;
pref_video_autoplay?: boolean;
in_chat?: boolean;
has_android_subscription?: boolean;
in_redesign_beta?: boolean;
icon_img: string;
has_mod_mail?: boolean;
pref_nightmode?: boolean;
awardee_karma?: number;
hide_from_robots?: boolean;
password_set?: boolean;
modhash?: null;
link_karma: number;
force_password_reset?: boolean;
total_karma: number;
inbox_count?: number;
pref_top_karma_subreddits?: boolean;
has_mail?: boolean;
pref_show_snoovatar?: boolean;
name: string;
pref_clickgadget?: number;
created: number;
has_verified_email: boolean;
gold_creddits?: number;
created_utc: number;
has_ios_subscription?: boolean;
pref_show_twitter?: boolean;
in_beta?: boolean;
comment_karma: number;
accept_followers: boolean;
has_subscribed: boolean;
accept_pms?: boolean;
}
};

View File

@ -0,0 +1,37 @@
import axios, { AxiosError } from 'axios';
import SessionManager from '../session/SessionManager';
import { RedditUser } from '../model/User';
export class RedditService {
private sessionManager: SessionManager;
constructor(sessionManager: SessionManager) {
this.sessionManager = sessionManager;
}
async getUserInfo(username: string): Promise<RedditUser> {
try {
const response = await this.sessionManager.axiosInstance.get(`/user/${username}/about`);
return response.data;
} catch (error) {
console.error('Error fetching user info:', error);
throw error;
}
}
async sendMessage(username: string, subject: string, message: string): Promise<void> {
try {
console.log(`await this.sessionManager.axiosInstance.post('/api/compose', {\n\tapi_type: 'json',\n\tto: ${username},\n\tsubject: ${subject},\n\ttext: ${message},\n});`)
//await this.sessionManager.axiosInstance.post('/api/compose', {
// api_type: 'json',
// to: username,
// subject: subject,
// text: message,
//});
console.log(`Message sent to ${username}`);
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}
}

View File

@ -0,0 +1,108 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
const qs = require('qs');
import dotenv from 'dotenv';
import axiosRetry from 'axios-retry';
import axiosThrottle from 'axios-request-throttle';
import { DatabaseService } from '../../db/services/Database';
dotenv.config();
class RedditSessionManager {
private static instance: RedditSessionManager;
public axiosInstance: AxiosInstance;
private constructor() {
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
this.axiosInstance = axios.create({
baseURL: 'https://oauth.reddit.com/', // Base URL for OAuth2 Reddit API
headers: {
'User-Agent': 'CrossTalk PM/0.1 by Whitneywisconson'
}
});
axiosRetry(this.axiosInstance, {
retries: 3,
retryDelay: this.retryDelayStrategy,
retryCondition: this.retryCondition,
});
}
public static async getInstance(): Promise<RedditSessionManager> {
if (!RedditSessionManager.instance) {
RedditSessionManager.instance = new RedditSessionManager();
await RedditSessionManager.instance.initializeAuthentication();
}
return RedditSessionManager.instance;
}
private async initializeAuthentication() {
// Check the database for an existing token
const currentToken = await DatabaseService.getCurrentOAuthToken(this.axiosInstance.defaults.baseURL as string);
if (currentToken && new Date() < new Date(currentToken.expiry_timestamp * 1000)) {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${currentToken.access_token}`;
console.log('Using existing Reddit API token from database.');
return;
}
console.log('No current Reddit API token from database requesting one')
// Authenticate with Reddit API to get a new token
await this.authenticate();
}
private async authenticate() {
if (!process.env.redditUsername) throw 'No Reddit Username Found in .env'
if (!process.env.redditPassword) throw 'No Reddit Password Found in .env'
const redditUsername = process.env.redditUsername as string
const redditPassword = process.env.redditPassword as string
const credentials = qs.stringify({
grant_type: 'password',
username: redditUsername,
password: redditPassword,
});
const authString = `${process.env.redditClientId}:${process.env.redditSecret}`;
const buffer = Buffer.from(authString);
const base64AuthString = buffer.toString('base64');
try {
const response = await this.axiosInstance.post('https://www.reddit.com/api/v1/access_token', credentials, {
headers: {
'User-Agent': `CrossTalk PM/0.1 by ${redditUsername}`, //TODO Dynamically set app name here
'Authorization': `Basic ${base64AuthString}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// Upsert the new token into the database
await DatabaseService.upsertOAuthToken(
this.axiosInstance.defaults.baseURL as string,
{
access_token: response.data.access_token,
token_type: response.data.token_type,
expires_in: response.data.expires_in,
scope: response.data.scope,
});
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${response.data.access_token}`;
console.log('Reddit API authenticated successfully.');
} catch (error) {
console.error('Error authenticating with Reddit API:', error);
}
}
private retryDelayStrategy(retryCount: number, error: AxiosError): number {
const retryAfter = error.response?.headers['retry-after'];
if (retryAfter) {
console.log(`429 Retry After: ${retryAfter}`);
return +retryAfter * 1000;
}
return Math.pow(2, retryCount) * 2000;
}
private retryCondition(error: AxiosError): boolean {
const status = error.response?.status ?? 0;
return status === 429 || status >= 400;
}
}
export default RedditSessionManager;

View File

@ -1,41 +1,93 @@
import fs from 'fs';
import path from 'path';
/**
* Manages the retrieval and formatting of messages stored in text files.
* This class provides functionality to load messages for rDrama and Reddit,
* select a random message, and replace placeholders within that message
* with specified values.
*/
export class MessageService {
private redditMessages: string[] = [];
private rdramaMessages: string[] = [];
constructor() {
this.loadMessages();
}
private loadMessages() {
/**
* Loads rDrama messages from a text file, splitting by a specific delimiter.
* Each message is separated by '---END---' in the text file.
*
* @example
* const rdramaMessages = MessageService.loadRdramaMessages();
*
* @returns {string[] | undefined} An array of rDrama messages, or undefined if there was an error loading the messages.
*/
private static loadRdramaMessages(): string[] | undefined {
try {
const redditMessagesPath = path.join(__dirname, 'messages', 'reddit_messages.txt');
this.redditMessages = fs.readFileSync(redditMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
} catch (error) {
console.error('Failed to load Reddit messages:', error);
}
try {
const rdramaMessagesPath = path.join(__dirname, 'messages', 'rdrama_messages.txt');
this.rdramaMessages = fs.readFileSync(rdramaMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
const rdramaMessagesPath = path.join(__dirname, '..', 'messages', 'rdrama_messages.txt');
return fs.readFileSync(rdramaMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
} catch (error) {
console.error('Failed to load rDrama messages:', error);
}
}
public getRandomRedditMessage(placeholders: { [key: string]: string }): string {
const message = this.redditMessages[Math.floor(Math.random() * this.redditMessages.length)];
/**
* Loads Reddit messages from a text file, splitting by a specific delimiter.
* Each message is separated by '---END---' in the text file.
*
* @example
* const redditMessages = MessageService.loadRedditMessages();
*
* @returns {string[] | undefined} An array of Reddit messages, or undefined if there was an error loading the messages.
*/
private static loadRedditMessages(): string[] | undefined {
try {
const redditMessagesPath = path.join(__dirname, '..', 'messages', 'reddit_messages.txt');
return fs.readFileSync(redditMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
} catch (error) {
console.error('Failed to load Reddit messages:', error);
}
}
/**
* Selects a random Reddit message from the loaded messages and replaces placeholders within it.
*
* @example
* const message = MessageService.getRandomRedditMessage({ username: 'exampleUser' });
*
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string | undefined} A formatted Reddit message with placeholders replaced, or undefined if messages couldn't be loaded.
*/
public static getRandomRedditMessage(placeholders: { [key: string]: string }): string | undefined {
const redditMessages = this.loadRedditMessages()
if (!redditMessages) return
const message = redditMessages[Math.floor(Math.random() * redditMessages.length)];
return this.replacePlaceholders(message, placeholders);
}
public getRandomRdramaMessage(placeholders: { [key: string]: string }): string {
const message = this.rdramaMessages[Math.floor(Math.random() * this.rdramaMessages.length)];
/**
* Selects a random rDrama message from the loaded messages and replaces placeholders within it.
*
* @example
* const message = MessageService.getRandomRdramaMessage({ username: 'exampleUser' });
*
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string | undefined} A formatted rDrama message with placeholders replaced, or undefined if messages couldn't be loaded.
*/
public static getRandomRdramaMessage(placeholders: { [key: string]: string }): string | undefined {
const rdramaMessages = this.loadRdramaMessages()
if (!rdramaMessages) return
const message = rdramaMessages[Math.floor(Math.random() * rdramaMessages.length)];
return this.replacePlaceholders(message, placeholders);
}
private replacePlaceholders(message: string, placeholders: { [key: string]: string }): string {
/**
* Replaces placeholders in a message with values from a provided mapping.
*
* @example
* const formattedMessage = MessageService.replacePlaceholders('Hello, {username}!', { username: 'exampleUser' });
*
* @param {string} message - The message containing placeholders.
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string} The message with placeholders replaced by actual values.
*/
private static replacePlaceholders(message: string, placeholders: { [key: string]: string }): string {
return Object.keys(placeholders).reduce((acc, key) => {
const regex = new RegExp(`{${key}}`, 'g');
return acc.replace(regex, placeholders[key]);

View File

@ -0,0 +1,28 @@
import dotenv from 'dotenv';
import { RedditService } from '../reddit/services/Reddit';
import { DatabaseService } from '../db/services/Database';
// Load environment variables from .env file
dotenv.config();
export async function shouldNotifyUser(username: string, redditService: RedditService): Promise<boolean> {
const userInfo = await redditService.getUserInfo(username);
if (!userInfo) return false;
const { is_mod, is_employee, accept_pms, total_karma } = userInfo.data;
const excludeMods = process.env.EXCLUDE_MODS !== 'false'; // Defaults to true unless explicitly set to 'false'
const excludeEmployees = process.env.EXCLUDE_EMPLOYEES !== 'false'; // Defaults to true unless explicitly set to 'false'
const notifyAcceptPms = accept_pms !== false; // Notify if accept_pms is true or undefined
const karmaThreshold = parseInt(process.env.KARMA_THRESHOLD || '100000', 10);
const hasBeenNotifiedBefore = await DatabaseService.userMentionExists(username);
const meetsCriteria =
(!excludeMods || !is_mod) && // Notify unless we're excluding mods and the user is a mod
(!excludeEmployees || !is_employee) && // Notify unless we're excluding employees and the user is an employee
notifyAcceptPms &&
total_karma < karmaThreshold &&
!hasBeenNotifiedBefore;
return meetsCriteria;
}

View File

@ -1,11 +1,17 @@
import { CommentProcessor } from "../rdrama/services/CommentProcessor";
import { CommentParser } from "../rdrama/services/CommentParser";
import { CommentPoster } from "../rdrama/services/CommentPoster";
import { MessageService } from "../utils/MessageService";
import { DatabaseService } from "../db/services/Database";
import { RedditService } from "../reddit/services/Reddit";
import RedditSessionManager from "../reddit/session/SessionManager";
import { shouldNotifyUser } from "../utils/ShouldNotify";
class WorkflowOrchestrator {
constructor(
private commentProcessor: CommentProcessor,
private commentParser: CommentParser,
//private redditNotifier: RedditNotifier // Handles notifications to Reddit users
private commentPoster: CommentPoster,
) { }
/**
@ -22,42 +28,47 @@ class WorkflowOrchestrator {
const uniqueUsernames = [...new Set(allUsernames)];
console.log(`Extracted ${uniqueUsernames.length} unique usernames`);
//// Query user information based on usernames
//const userInfo = await this.databaseService.queryUsersInfo(uniqueUsernames);
//console.log(`Queried information for ${userInfo.length} users`);
//
//// Filter users who should be notified
//const usersToNotify = userInfo.filter(user => this.shouldNotifyUser(user));
//console.log(`Identified ${usersToNotify.length} users to notify`);
//
//// Notify users
//for (const user of usersToNotify) {
// await this.redditNotifier.notifyUser(user);
// console.log(`Notified user: ${user.username}`);
//}
for (const comment of comments) {
const redditUsers = this.commentParser.extractUsernames(comment)
if (redditUsers.length === 0) continue
console.log('found:', redditUsers)
const placeholdersRdrama = {
author_name: comment.author_name,
};
for (const redditUser of redditUsers) {
const userMentionExists = await DatabaseService.userMentionExists(redditUser)
if (userMentionExists) continue
const commentResponseRdrama = MessageService.getRandomRdramaMessage(placeholdersRdrama)
if (!commentResponseRdrama) throw new Error('No comments for Rdrama found')
const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, `##### TEST MESSAGE NO REDDITOR PINGED (YET...)\n${commentResponseRdrama}`)
//const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, ${commentResponse}`) //TODO uncomment after golive
console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4))
const redditSession = await RedditSessionManager.getInstance()
const redditService = new RedditService(redditSession)
const resultshouldNotifyUser = await shouldNotifyUser(redditUser, redditService)
if (!resultshouldNotifyUser) continue
const placeholdersReddit = {
author_name: comment.author_name,
username: redditUser,
permalink: comment.permalink
};
const redditMessage = MessageService.getRandomRedditMessage(placeholdersReddit)
if (!redditMessage) throw new Error('No comments for Reddit found')
await DatabaseService.insertUserMention({
rdrama_comment_id: comment.id,
username: redditUser,
message: redditMessage,
})
await redditService.sendMessage(redditUser, 'Crosstalk PM Notification', redditMessage)
return;
}
}
console.log('Workflow executed successfully.');
} catch (error) {
console.error('An error occurred during workflow execution:', error);
}
}
/**
* Determines whether a user should be notified based on certain criteria.
*
* @param user - The user information object.
* @returns A boolean indicating whether the user should be notified.
*/
//private shouldNotifyUser(user: UserInfo): boolean {
// // Placeholder for the actual logic to determine if a user should be notified.
// // This could involve checking the last notification time against the current time,
// // user preferences, or other criteria defined in the business logic.
//
// // Example logic (to be replaced with actual implementation):
// const lastNotifiedTime = new Date(user.lastNotified); // Assuming 'lastNotified' is a Date or string.
// const notificationThreshold = 24 * 60 * 60 * 1000; // 24 hours in milliseconds.
// return (Date.now() - lastNotifiedTime.getTime()) > notificationThreshold;
//}
}
export default WorkflowOrchestrator;