606 Commits
v1.0.13 ... js

Author SHA1 Message Date
Ajay Ramachandran
0002a7549d Merge pull request #185 from LoganDark/patch-1
Correct indentation in _private.db.sql
2020-11-24 13:21:11 -05:00
LoganDark
b4b7b55ce1 Correct indentation in _private.db.sql 2020-11-24 05:15:13 -08:00
Ajay Ramachandran
5c753db661 Merge pull request #184 from ajayyy/dependabot/npm_and_yarn/dot-prop-4.2.1
Bump dot-prop from 4.2.0 to 4.2.1
2020-11-23 12:40:19 -05:00
dependabot[bot]
d6977b7be0 Bump dot-prop from 4.2.0 to 4.2.1
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-23 17:30:35 +00:00
Ajay Ramachandran
97bb7b534a Increase max segments from 8 to 32 2020-11-23 12:29:57 -05:00
Ajay Ramachandran
69e7b35abb Merge pull request #182 from Joe-Dowd/bugfix/no-revive-nosegments-unban
Don't revive segments marked with nosegments on unbanning a user
2020-10-28 23:13:46 -04:00
Ajay Ramachandran
b3123f4d4a Merge pull request #181 from Joe-Dowd/test-update-with-comments
(WIP) Tests needed: Should be updated to reflect new voting limit.
2020-10-28 23:11:45 -04:00
Joe Dowd
9d293b2cb4 Don't revive segments for videos marked with noSegment when un-banning a user 2020-10-29 01:03:07 +00:00
Joe Dowd
bb47181daa removed tests no longer needed - should be updated to reflect new changes 2020-10-28 11:44:11 +00:00
Ajay Ramachandran
009a489d3a Fix typo 2020-10-27 19:07:48 -04:00
Ajay Ramachandran
06fa6eb874 Raise category change requirement 2020-10-27 18:58:21 -04:00
Ajay Ramachandran
c462323dd5 Merge pull request #178 from FoseFx/fosefx-errorh
Improve missing fields checks (POST skipSegment)
2020-10-21 10:57:43 -04:00
Max Baumann
967715ab3b add new missing field checks 2020-10-21 16:31:13 +02:00
Ajay Ramachandran
7b818154dd Merge pull request #176 from ajayyy/experimental
Don't fail when the YouTube API errors
2020-10-18 01:09:13 -04:00
Ajay Ramachandran
4f6ff4d177 Don't fail when the YouTube API errors 2020-10-18 01:08:52 -04:00
Ajay Ramachandran
0968394c84 Merge pull request #170 from ajayyy/experimental
Attach private db for read only
2020-10-15 14:27:17 -04:00
Ajay Ramachandran
809b09a1c2 Attach private db for read only 2020-10-15 14:25:34 -04:00
Ajay Ramachandran
b5f93edc7e Merge pull request #169 from ajayyy/experimental
Increase max username size
2020-10-15 11:37:31 -04:00
Ajay Ramachandran
0a72a64489 Don't cache when cacheTimeMs is NaN 2020-10-15 11:33:47 -04:00
Ajay Ramachandran
d6ca579495 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental 2020-10-15 11:32:25 -04:00
Ajay Ramachandran
34b8119a21 Increase max username size 2020-10-14 23:12:04 -04:00
Ajay Ramachandran
ad74c2c710 Merge pull request #163 from thignus/reject-80percent-total-submissions
reject submissions if total length is more than 80 precent of the video
2020-10-14 19:43:16 -04:00
James Robinson
064d3ad5d5 fix test imports 2020-10-14 18:40:55 -05:00
Ajay Ramachandran
1f584984f4 Merge pull request #156 from MRuy/147-add-cache-for-gettopusers
Add cache for getTopUsers
2020-10-14 18:48:47 -04:00
Ajay Ramachandran
779b79c54b Merge pull request #165 from ajayyy/testing
Testing
2020-10-14 18:47:55 -04:00
Ajay Ramachandran
4c222ce2cc Merge branch 'master' into reject-80percent-total-submissions 2020-10-14 18:41:55 -04:00
Ajay Ramachandran
a7a9c0cdf6 Merge pull request #164 from Joe-Dowd/delete-nosegment
added delete nosegment endpoint
2020-10-14 18:40:22 -04:00
Ajay Ramachandran
e979cbba4e Merge branch 'master' into 147-add-cache-for-gettopusers 2020-10-14 18:35:15 -04:00
Ajay Ramachandran
d0b34e057b Merge pull request #143 from MRuy/feature/getuserinfo
Added getUserInfo endpoint
2020-10-14 18:34:33 -04:00
Ajay Ramachandran
e4045952c8 Merge branch 'master' into feature/getuserinfo 2020-10-14 18:34:27 -04:00
Ajay Ramachandran
678e4187c5 Merge pull request #154 from MRuy/feature-segmentshift
Added segmentShift
2020-10-14 18:27:09 -04:00
James
d1d04b3fb4 Merge pull request #1 from Joe-Dowd/reject-80percent-total-submissions
made youtube api cache have constant part value
2020-10-14 14:56:22 -05:00
Joe Dowd
830ef7e0dc made youtube api cache have conatcnt part value 2020-10-14 20:52:01 +01:00
Joe Dowd
2426b51512 added delete nosegment endpoint 2020-10-14 20:18:31 +01:00
James Robinson
bcf90e8094 fix formatting and add more comments 2020-10-14 00:43:29 -05:00
James Robinson
2825cb63fb ignore segments with less than -1 votes 2020-10-14 00:33:00 -05:00
James Robinson
85dd187cb0 reject submissions if total length is more than 80 precent of the video 2020-10-14 00:25:05 -05:00
Ajay Ramachandran
0e71f80a78 Merge pull request #162 from ajayyy/experimental
Don't allow dots in videoID
2020-10-11 14:13:19 -04:00
Ajay Ramachandran
e0df3d4208 Don't allow dots in videoID 2020-10-11 14:11:20 -04:00
Ajay Ramachandran
62a047337c Merge pull request #161 from ajayyy/testing
Testing
2020-10-11 13:36:41 -04:00
Ajay Ramachandran
c25d66b11b Merge pull request #160 from ajayyy/experimental
Fix example error codes
2020-10-11 13:36:11 -04:00
Ajay Ramachandran
11eb742540 Uncomment code 2020-10-11 13:29:54 -04:00
Ajay Ramachandran
d7763b688d Fix example error codes 2020-10-11 13:14:52 -04:00
Ajay Ramachandran
9aba0560a1 Fix indentation 2020-10-11 13:13:16 -04:00
Ajay Ramachandran
70bc239d01 Merge pull request #159 from ajayyy/experimental
Setup different ratelimit for views and votes
2020-10-11 13:12:49 -04:00
Ajay Ramachandran
a5f5f72346 Setup different ratelimit for views and votes 2020-10-11 13:07:57 -04:00
Ajay Ramachandran
73b7332639 Add vote rate limit to views as well 2020-10-11 12:42:02 -04:00
Ajay Ramachandran
64f93cec65 Merge pull request #157 from MRuy/add-vote-rate-limit
Add vote rate limit
2020-10-11 12:39:42 -04:00
Nanobyte
58097f0d60 Add vote rate limit 2020-10-11 16:17:17 +02:00
Nanobyte
41dc16453e Update src/utils/createMemoryCache.js
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-10-09 08:58:31 +02:00
Nanobyte
fb7ff50feb Update src/utils/createMemoryCache.js
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-10-09 08:58:15 +02:00
Ajay Ramachandran
97383a71af Merge pull request #155 from MRuy/cleanup-uuid-generation
Submission UUID generation moved to function
2020-10-08 22:23:35 -04:00
Ajay Ramachandran
03a142ff62 Merge pull request #152 from Joe-Dowd/nosegments-404-to-array
get segments by hash 404 to array
2020-10-08 22:22:46 -04:00
Joe Dowd
b244b1e1ad reverted test to 404 2020-10-06 23:25:16 +01:00
Joe Dowd
62916f6a7e reverted to 404 sith empty array 2020-10-06 23:22:48 +01:00
Nanobyte
25b91af8bc Add cache for getTopUsers
See #147
2020-10-06 23:25:11 +02:00
Nanobyte
cce20319ad Submission UUID generation moved to function
See issue #139
2020-10-06 15:59:32 +02:00
Nanobyte
8e33fdf49f Added segmentShift
See #151
2020-10-04 21:37:35 +02:00
Joe Dowd
fe91d13ff3 removed old functionality comments 2020-10-01 00:53:13 +01:00
Joe Dowd
4cea3c2a3b Added [] return rather than 404 for no segments with matching hash 2020-10-01 00:51:58 +01:00
Ajay Ramachandran
7dcdc883e4 Fix no segments error message typo 2020-09-25 16:57:24 -04:00
Ajay Ramachandran
31fef27ffd Fix no segments error formatting 2020-09-25 14:50:15 -04:00
Ajay Ramachandran
50043e6574 Merge pull request #149 from ajayyy/experimental
Improve no segments error message
2020-09-25 14:39:13 -04:00
Ajay Ramachandran
27947dc65f Improve no segments error message 2020-09-25 14:36:16 -04:00
Ajay Ramachandran
5cfe974310 Merge pull request #148 from ajayyy/experimental
Fixes to category voting and prevent category votes on VIP submissions.
2020-09-23 14:09:16 -04:00
Ajay Ramachandran
c6afaf81e0 Properly catch user counter errors 2020-09-23 14:05:44 -04:00
Ajay Ramachandran
61b6b90f77 Fix tests 2020-09-23 13:20:01 -04:00
Ajay Ramachandran
36baeed86e Fixes to category voting and prevent category votes on VIP submissions.
Also makes submitter category count as a category vote
2020-09-23 11:45:56 -04:00
Ajay Ramachandran
7c0bc9afd3 Changed VIP to moderator in no segments error 2020-09-19 18:54:49 -04:00
Ajay Ramachandran
138f843f5d Updated no segments error message 2020-09-19 18:51:13 -04:00
Ajay Ramachandran
689c0f7b31 Allow VIPs to submit locked categories 2020-09-19 13:59:34 -04:00
Ajay Ramachandran
5d731463f2 Change no segments error message 2020-09-19 13:53:40 -04:00
Nanobyte
36f654f41c Blocking users with too many active warnings from submitting votes and submissions 2020-09-16 22:40:11 +02:00
Ajay Ramachandran
3c79c0f7a8 Remove extra test case 2020-09-16 13:48:54 -04:00
Ajay Ramachandran
abc91cc7a5 Merge pull request #146 from ajayyy/testing
Don't send webhooks for duplicate votes
2020-09-16 09:32:09 -04:00
Ajay Ramachandran
211562a0d6 Don't send webhooks for duplicate votes 2020-09-16 09:30:26 -04:00
Joe Dowd
075cb9d5f2 added warning system
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-09-16 00:56:10 +02:00
Ajay Ramachandran
2955ac3c9b Merge pull request #145 from ajayyy/testing
Fix removing undo vote
2020-09-15 13:13:16 -04:00
Ajay Ramachandran
a7933c925b Fix removing undo vote 2020-09-15 13:10:55 -04:00
Nanobyte
b52a862f4f Merge branch 'master' into feature/getuserinfo 2020-09-15 19:08:56 +02:00
Ajay Ramachandran
1fe68c66c5 Merge pull request #140 from ajayyy/testing
Testing
2020-09-15 12:50:52 -04:00
Ajay Ramachandran
be86f43947 Added default options 2020-09-15 12:22:28 -04:00
Ajay Ramachandran
a86dc0fc7b Changed redis polyfill to not throw an error 2020-09-15 12:16:34 -04:00
Ajay Ramachandran
eaaf3b8812 Add pollyfill for redis set 2020-09-15 12:15:23 -04:00
Ajay Ramachandran
d3463bbef2 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into testing
# Conflicts:
#	src/routes/postSkipSegments.js
#	test/cases/voteOnSponsorTime.js
2020-09-15 11:56:14 -04:00
Nanobyte
43db13ab3f Added getUserInfo endpoint
This endpoint provides the following data about the user:

* userID
* userName
* minutesSaved
* segmentCount
* viewCount
2020-09-14 09:17:35 +02:00
Ajay Ramachandran
4a865c4fb8 Merge pull request #142 from ajayyy/experimental
Don't call webhooks twice
2020-09-13 18:28:41 -04:00
Ajay Ramachandran
70a526fb4f Don't call webhooks twice 2020-09-13 18:27:02 -04:00
Ajay Ramachandran
0839c6b6f9 Merge pull request #81 from andrewzlee/nb-mod-fetch
Add NB to automod process
2020-09-13 11:15:35 -04:00
Ajay Ramachandran
7a83e7901a Merge pull request #131 from ajayyy/query-by-hash-prefix
Hash prefix fixes
2020-09-11 17:45:58 -04:00
Ajay Ramachandran
928d41c52d Merge pull request #136 from Joe-Dowd/hash-prefix-db
Hash prefix db
2020-09-11 17:44:48 -04:00
Ajay Ramachandran
fdc20e28ff Merge pull request #134 from Joe-Dowd/youtube-cache
Youtube cache
2020-09-11 17:37:21 -04:00
Ajay Ramachandran
0c14560eaa Merge branch 'master' into nb-mod-fetch 2020-09-11 17:32:14 -04:00
Ajay Ramachandran
a49db96d49 Clarify error message 2020-09-11 17:30:54 -04:00
Ajay Ramachandran
66642e7bdf Merge pull request #138 from ajayyy/experimental
Fix error messages in postSkipSegments
2020-09-11 17:22:52 -04:00
Andrew Lee
6843e22a7b Change automod check to call YT and NB API a single time per submission 2020-09-11 17:08:41 -04:00
Ajay Ramachandran
4baf13f82c Fix error messages in postSkipSegments 2020-09-11 16:35:12 -04:00
Ajay Ramachandran
f80e6887fe Merge pull request #137 from ajayyy/experimental
Add error message for 400 errors
2020-09-09 21:36:21 -04:00
Ajay Ramachandran
548061ff13 Add error message for 400 errors 2020-09-09 21:32:47 -04:00
Ajay Ramachandran
9d9cb4f139 Merge pull request #135 from MRuy/checking-categories
Adding check to only allow a list of categories
2020-09-09 20:20:04 -04:00
Joe Dowd
0611ad54d0 fixed db upgrade filesize inflation 2020-09-09 23:14:20 +01:00
Joe Dowd
29a6528e31 Merge branch 'testing' of github.com:ajayyy/SponsorBlockServer into testing 2020-09-09 22:51:05 +01:00
Nanobyte
3bf92fe2ad Merge pull request #1 from Joe-Dowd/checking-categories
added test for change category
2020-09-09 23:00:29 +02:00
Joe Dowd
a068e194e9 added test for change category 2020-09-09 21:57:59 +01:00
Nanobyte
6485fd0f88 Update test.json 2020-09-09 18:57:47 +02:00
Nanobyte
dec0971c14 Adding check to only allow a list of categories 2020-09-09 16:40:13 +02:00
Joe Dowd
bbe31149b4 cleaner logging 2020-09-08 17:43:51 +01:00
Joe Dowd
b7ea0fa681 remove logging 2020-09-08 17:39:53 +01:00
Joe Dowd
d72a0ca009 Merge branch 'testing' of github.com:ajayyy/SponsorBlockServer into youtube-cache 2020-09-08 17:38:47 +01:00
Joe Dowd
9d94067cd0 Added youtube api cache (optional) 2020-09-08 17:37:29 +01:00
Andrew Lee
137a6dc771 Make db queries one request 2020-09-07 13:16:03 -04:00
Andrew Lee
ce36806169 Added usernames to NB Discord webhooks 2020-09-07 12:29:13 -04:00
Ajay Ramachandran
6fe8dfc188 Merge pull request #133 from ajayyy/experimental
Don't fail when YouTube API fails
2020-09-07 10:25:27 -04:00
Ajay Ramachandran
cb386b7f8d Don't fail when YouTube API fails 2020-09-07 10:23:51 -04:00
Andrew Lee
8b132c37a0 Disable NB automod vote decrease 2020-09-06 21:10:28 -04:00
Andrew Lee
6e8d644574 Removed unnecessary YT API call from NB Discord webhook 2020-09-06 21:08:51 -04:00
Ajay Ramachandran
1536cc085c Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into testing 2020-09-06 13:37:23 -04:00
Ajay Ramachandran
87c7ec405d Merge pull request #132 from ajayyy/experimental
Made report by submitter work for VIPs
2020-09-06 13:37:10 -04:00
Ajay Ramachandran
9bcecae9a2 Made report by submitter work for VIPs 2020-09-06 13:35:14 -04:00
Andrew Lee
fbd8113217 Bug fix automod params 2020-09-05 18:26:20 -04:00
Andrew Lee
66c3109037 Add Discord webhook for videos that fail NB check 2020-09-05 18:07:43 -04:00
Andrew Lee
e8116de4bb Update configs for NB discord URLs 2020-09-05 17:03:52 -04:00
Ajay Ramachandran
c3de215b08 Add index for hashed videoID 2020-09-04 13:49:28 -04:00
Ajay Ramachandran
8de7801662 Removed extra test case 2020-09-04 12:34:05 -04:00
Ajay Ramachandran
ac80b2322b Fix broken shadow ban check 2020-09-04 12:16:13 -04:00
Ajay Ramachandran
70632dd33a Merge pull request #127 from Joe-Dowd/query-by-hash-prefix
Query by hash prefix
2020-09-04 11:47:55 -04:00
Ajay Ramachandran
9ee5c508e4 Switch to null instead of undefined and fix indentation 2020-09-03 23:54:43 -04:00
Ajay Ramachandran
008963156d Merge pull request #114 from Joe-Dowd/no-segments
Mark video as containing no segments
2020-09-03 23:42:58 -04:00
Joe Dowd
b79f740e2b Merge pull request #6 from Joe-Dowd/docker-fix
add db schemas to docker image
2020-09-03 23:08:04 +01:00
Joe Dowd
11b0cfb536 add db schemas to docker image 2020-09-03 23:05:13 +01:00
Joe Dowd
6abf556234 Merge pull request #5 from ajayyy/testing
Testing
2020-09-03 22:27:18 +01:00
Joe Dowd
75de91ae1c Merge pull request #4 from Joe-Dowd/query-by-hash-prefix
Query by hash prefix
2020-09-03 22:26:46 +01:00
Joe Dowd
6dd735eab0 Merge pull request #3 from Joe-Dowd/no-segments
fixed db update, started no segments
2020-09-03 22:26:33 +01:00
Ajay Ramachandran
dfeb54d7f1 Merge branch 'experimental' of https://github.com/ajayyy/SponsorBlockServer 2020-09-03 12:05:01 -04:00
Ajay Ramachandran
a053d87bd2 Catch user counter errors 2020-09-03 12:04:49 -04:00
Joe Dowd
e33062feeb updated endpoints 2020-09-01 21:37:54 +01:00
Ajay Ramachandran
b0dc79d071 Send max of API users or extension users 2020-08-31 22:17:24 -04:00
Ajay Ramachandran
19f092bcbb Merge pull request #128 from ajayyy/experimental
Allow all VIP to ban
2020-08-31 14:06:14 -04:00
Ajay Ramachandran
fd81096f63 Allow all VIP to ban 2020-08-31 14:03:12 -04:00
Joe Dowd
2c32460a6e more tests 2020-08-31 04:17:50 +01:00
Joe Dowd
9c9c2a23cc added comment to db upgrade sql file 2020-08-31 03:31:53 +01:00
Joe Dowd
2067599843 added test for hash prefix length 2020-08-31 03:27:12 +01:00
Joe Dowd
00534d91d4 added underscore to category format check 2020-08-31 03:09:47 +01:00
Joe Dowd
88e6c6f93c added db upgrade tests 2020-08-31 02:52:12 +01:00
Joe Dowd
82d59e159f added post and get test to hash prefix 2020-08-31 02:42:25 +01:00
Joe Dowd
aa878482d3 removed status from no segment responses 2020-08-31 01:55:38 +01:00
Joe Dowd
754d3762df fixed test category name 2020-08-31 01:50:36 +01:00
Joe Dowd
1e643c1c07 categorys -> categories 2020-08-31 01:48:41 +01:00
Joe Dowd
36ce803828 Update src/routes/getIsUserVIP.js
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-08-31 01:44:30 +01:00
Joe Dowd
28bd24022b removed schema comments and removed test log 2020-08-31 01:08:18 +01:00
Joe Dowd
84c8eeccc6 added hash prefix test for missing categories 2020-08-31 01:06:50 +01:00
Joe Dowd
1a06502806 added get segments by hash prefix 2020-08-31 00:45:06 +01:00
Joe Dowd
26c72b006c mreged no-segments 2020-08-30 22:33:56 +01:00
Joe Dowd
b8d7eef536 origin testing merge 2020-08-30 22:24:37 +01:00
Joe Dowd
5f23fdd590 Added tests and finished no-segments interface. 2020-08-30 21:47:02 +01:00
Joe Dowd
84533b544a Merge testing & bufgix on logging proxy submissions 2020-08-30 20:37:24 +01:00
Ajay Ramachandran
673c3c13e7 Merge pull request #125 from ajayyy/experimental
Fixed first time submissions webhook only sending for non first time submissions
2020-08-30 11:55:57 -04:00
Ajay Ramachandran
168229e602 Fixed first time submissions webhook only sending for non first time submissions 2020-08-30 11:55:19 -04:00
Ajay Ramachandran
f2265f0dce Merge pull request #124 from ajayyy/experimental
Fix all submissions being sent to first time submissions channel
2020-08-30 11:35:13 -04:00
Ajay Ramachandran
9ea98b2e2b Fix all submissions being sent to first time submissions channel 2020-08-30 11:33:44 -04:00
Ajay Ramachandran
48bb93e8d4 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer 2020-08-30 11:27:14 -04:00
Ajay Ramachandran
24cdbaeb51 Merge pull request #123 from ajayyy/experimental
Only accept new user count if higher + add to example config
2020-08-30 11:26:53 -04:00
Ajay Ramachandran
febfab40de Merge branch 'experimental' of https://github.com/ajayyy/SponsorBlockServer 2020-08-30 11:18:05 -04:00
Ajay Ramachandran
89ee0afd61 Only accept new user count if higher + add to example config 2020-08-30 11:17:26 -04:00
Ajay Ramachandran
ffa6b4fac8 Merge pull request #122 from ajayyy/experimental
Added option to use new user counter
2020-08-30 00:25:56 -04:00
Ajay Ramachandran
e46444f3b9 Added option to use new user counter 2020-08-30 00:22:21 -04:00
Ajay Ramachandran
f4afe00f15 Merge pull request #121 from thignus/fix-set-username
fix username variable in setUsername
2020-08-29 23:05:22 -04:00
James Robinson
92b0f917d6 fix username variable in setUsername 2020-08-29 22:01:13 -05:00
Ajay Ramachandran
ba68969943 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into nb-mod-fetch
# Conflicts:
#	src/routes/postSkipSegments.js
#	test/mocks.js
2020-08-29 21:03:13 -04:00
Andrew Lee
72ce7fcb56 Update test cases with new automod api 2020-08-29 20:41:07 -04:00
Andrew Lee
4df31aa6b0 Merge branch 'nb-mod-fetch' of https://github.com/andrewzlee/SponsorBlockServer into nb-mod-fetch 2020-08-29 20:38:59 -04:00
Ajay Ramachandran
262d3d3dfd Add username length limit 2020-08-29 20:05:49 -04:00
Ajay Ramachandran
49cfbdd95f Lower minimum votes to 20 2020-08-29 20:02:32 -04:00
Ajay Ramachandran
a161316dc2 Merge pull request #118 from TAG-Epic/custom-webhooks
Add custom webhooks
2020-08-29 19:26:13 -04:00
Ajay Ramachandran
800a3dff9d Send webhooks after response 2020-08-29 19:22:44 -04:00
Ajay Ramachandran
0872c727d5 Fix auto moderate merge issues 2020-08-29 18:58:52 -04:00
Andrew Lee
d965308346 Merge fixes to nb automod and api change 2020-08-29 18:23:04 -04:00
Andrew Lee
4ffb1e0cdc Merge branch 'nb-mod-fetch' of https://github.com/andrewzlee/SponsorBlockServer into nb-mod-fetch 2020-08-29 17:48:44 -04:00
Andrew Lee
8fc1fe7d8e Re-add node-fetch package 2020-08-29 17:48:39 -04:00
Andrew Lee
c6d28d7fc5 Apply suggestions from code review
Fix syntax changes caused by bad merge

Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-08-29 17:45:59 -04:00
Ajay Ramachandran
373548d396 Remove unused variable 2020-08-29 17:39:08 -04:00
Andrew Lee
f1c520fbe8 Merge branch 'master' into nb-mod-fetch 2020-08-29 16:37:20 -04:00
Ajay Ramachandran
a1babfb2ce Lower vote threshold 2020-08-29 15:48:31 -04:00
Ajay Ramachandran
407c38b4be Hostfix basic ip ban 2020-08-29 14:00:15 -04:00
Ajay Ramachandran
f868939eb9 Remove dead submissions from total stats 2020-08-29 12:43:17 -04:00
Ajay Ramachandran
84b143545d Fix function name 2020-08-29 12:35:24 -04:00
Ajay Ramachandran
938d88fa2b Merge branch 'experimental' of https://github.com/ajayyy/SponsorBlockServer 2020-08-29 12:26:02 -04:00
Ajay Ramachandran
67951b39d9 Change vote and username requirements 2020-08-29 12:25:35 -04:00
Ajay Ramachandran
4b5a0fb59d Merge pull request #120 from ajayyy/experimental
New vote weight
2020-08-24 20:56:55 -04:00
Ajay Ramachandran
19363c86f9 New vote weight 2020-08-24 20:51:04 -04:00
Ajay Ramachandran
c7daa72b4e Merge pull request #119 from ajayyy/experimental
Added decimals to formatted time
2020-08-24 20:23:17 -04:00
Ajay Ramachandran
f7bde024cb Allow self-upvotes on dead submissions 2020-08-24 20:21:39 -04:00
Ajay Ramachandran
f4b36867ff Fixed duplicate variable 2020-08-24 20:15:46 -04:00
Ajay Ramachandran
2f2273b8a8 Added decimals to formatted time 2020-08-24 20:12:57 -04:00
Epic
f7ea7061d6 Merge pull request #4 from Joe-Dowd/testing-custom-webhooks
Fixed undesolved test error
2020-08-24 23:52:00 +02:00
Joe Dowd
83ec23220c added wrong port test 2020-08-24 22:50:27 +01:00
Joe Dowd
dc7dadd9a1 tidy undesolved fix 2020-08-24 22:43:36 +01:00
Joe Dowd
3563924c6f Fixed undesolved test error 2020-08-24 22:40:02 +01:00
Epic
ec1a294ef0 Merge pull request #3 from Joe-Dowd/testing-custom-webhooks
add unresolvable host test - introduces tets faiulure
2020-08-24 23:32:40 +02:00
Joe Dowd
eb9282c60a add unresolvable host test - introduces tets faiulure 2020-08-24 22:22:02 +01:00
Epic
2dc5dea10b Merge pull request #2 from Joe-Dowd/testing-custom-webhooks
added failed webhook call to tests
2020-08-24 23:14:45 +02:00
Joe Dowd
3066a077c7 added failed webhook call to tests 2020-08-24 22:10:49 +01:00
Epic
72b317352f Merge pull request #1 from Joe-Dowd/testing-custom-webhooks
make tests hit webhook code
2020-08-24 23:06:55 +02:00
Joe Dowd
75a62a5729 make tests hit webhook code 2020-08-24 22:04:18 +01:00
TAG-Epic
3085b0e30c Remove random s 2020-08-24 22:45:47 +02:00
TAG-Epic
5c9aa8c9ca Cleaned up function names 2020-08-24 13:59:13 +02:00
TAG-Epic
2c70f87b93 Add webhooks to postSkipSegments 2020-08-24 13:14:04 +02:00
TAG-Epic
aca4318351 Move dispatchEvent to webhookUtils 2020-08-24 09:06:47 +02:00
TAG-Epic
16777c30ca Change .size to .length 2020-08-23 22:35:35 +02:00
TAG-Epic
a897c313fb Remove broken and unnecessary check 2020-08-23 22:33:06 +02:00
TAG-Epic
9eddc330c5 Move getVoteAuthor to webhookUtils and move user status (api) to webhookUtils 2020-08-23 22:30:35 +02:00
TAG-Epic
20d813d2ea Fix missing semicolon 2020-08-23 22:13:00 +02:00
TAG-Epic
14a990f7ea Remove duplicate isOwnSubmission 2020-08-23 22:09:02 +02:00
TAG-Epic
2597a57f3a Update user status to include self remove 2020-08-23 22:02:34 +02:00
TAG-Epic
7ae97e4c69 Change submission id to UUID 2020-08-23 21:45:39 +02:00
TAG-Epic
e5aa631da0 Refactor custom webhooks 2020-08-23 21:44:50 +02:00
TAG-Epic
a268f9a892 Update example & test config 2020-08-23 20:54:18 +02:00
TAG-Epic
77d08d2340 Use logger instead of console.log 2020-08-23 20:05:11 +02:00
TAG-Epic
2ffed72977 Fix merge conflicts 2020-08-23 20:02:18 +02:00
TAG-Epic
81cb93d03e Merge remote-tracking branch 'origin/master' into master 2020-08-23 20:00:59 +02:00
TAG-Epic
f7d162b955 Add custom webhooks for votes 2020-08-23 19:42:52 +02:00
Ajay Ramachandran
342a661606 Merge pull request #117 from ajayyy/experimental
Logging improvements
2020-08-23 12:48:15 -04:00
Ajay Ramachandran
ece475076e Added log colors and method name 2020-08-23 12:35:57 -04:00
Ajay Ramachandran
8474b74cbf Merge pull request #116 from ajayyy/experimental
Add author name to discord notification
2020-08-23 11:37:22 -04:00
Ajay Ramachandran
9cebb8769f Add author name to discord notification 2020-08-23 11:34:07 -04:00
Ajay Ramachandran
0289b71497 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental 2020-08-23 11:21:28 -04:00
Ajay Ramachandran
dc33a3a383 Merge pull request #115 from ajayyy/testing
Testing
2020-08-23 11:12:29 -04:00
Ajay Ramachandran
b59b0f5387 Merge pull request #113 from Joe-Dowd/remove-own-submission
Remove own submission with a downvote
2020-08-22 16:07:05 -04:00
Ajay Ramachandran
dc6c6fc5e9 Merge pull request #112 from Joe-Dowd/logging-change
Logging change
2020-08-22 16:05:48 -04:00
Joe Dowd
17649157c1 changed dockler default config to use sqlite 2020-08-22 20:53:32 +01:00
Joe Dowd
fba25fea0f Update src/routes/voteOnSponsorTime.js
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2020-08-22 20:45:31 +01:00
Joe Dowd
a5cf59d854 improved error handling 2020-08-22 20:39:13 +01:00
Joe Dowd
a971072cb8 reject submissions that have meen marked as invalid by a vip 2020-08-22 04:23:28 +01:00
Joe Dowd
c946d2309e add is VIP endpoint 2020-08-22 03:58:27 +01:00
Joe Dowd
6bde59c14a added extra sql test 2020-08-22 03:29:16 +01:00
Joe Dowd
db3de8ce6f changed copy/pasted comment 2020-08-22 03:21:10 +01:00
Joe Dowd
f53c541538 added ability for vips to submit segments not in a video 2020-08-22 03:05:51 +01:00
Joe Dowd
44ea0c418a fixed db update, started no segments 2020-08-22 02:14:19 +01:00
Joe Dowd
16c68dd51d allow removing own segments with a downvote 2020-08-21 22:32:34 +01:00
Joe Dowd
c1d4ba3c80 Merge pull request #2 from Joe-Dowd/logging-change
Added logger util and used in place of console log
2020-08-21 21:06:22 +01:00
Joe Dowd
226197a827 Added logger util and used in place of console log 2020-08-21 15:27:41 +01:00
Ajay Ramachandran
2516634856 Don't allow 0 ms submissions 2020-08-02 12:08:44 -04:00
Ajay Ramachandran
d5a720fa0c Check that start time is not less than zero 2020-08-02 11:58:11 -04:00
Joe Dowd
5ac5c30fd6 import config 2020-07-28 04:32:12 +01:00
Joe Dowd
f4db30e988 Merge branch 'testing' of github.com:Joe-Dowd/SponsorBlockServer into testing 2020-07-28 04:04:56 +01:00
Ajay Ramachandran
5fc839513e Merge pull request #105 from Joe-Dowd/proxy-submission
Proxy submission
2020-07-27 21:09:32 -04:00
Ajay Ramachandran
7878336866 Don't return after proxying 2020-07-27 20:53:15 -04:00
Ajay Ramachandran
cadf2640fe Add spacing 2020-07-27 19:54:38 -04:00
Ajay Ramachandran
98b79b042b Return when proxying 2020-07-27 19:54:08 -04:00
Ajay Ramachandran
51fe7d5599 Changed to if statement 2020-07-27 19:53:23 -04:00
Joe Dowd
99c8d5d238 removed log and fixed log typo 2020-07-27 05:56:23 +01:00
Joe Dowd
0e34423dd6 added option to example config 2020-07-27 05:53:16 +01:00
Joe Dowd
18f5d656de Aded option to proxy submisissions to another server to persist. 2020-07-27 05:51:49 +01:00
Ajay Ramachandran
22fc90713a Merge pull request #104 from ajayyy/experimental
Fix shadow hidden endpoint
2020-07-26 11:45:20 -04:00
Ajay Ramachandran
fe444560fd Fix shadow hidden endpoint 2020-07-26 11:44:15 -04:00
Ajay Ramachandran
0cda5db916 Merge pull request #103 from ajayyy/experimental
Improve user banning
2020-07-26 11:28:54 -04:00
Ajay Ramachandran
986c9dcf5f Don't show banned users on the leaderboard 2020-07-26 11:26:51 -04:00
Ajay Ramachandran
4bfa5e7de8 Banned users can't vote 2020-07-26 11:13:36 -04:00
Ajay Ramachandran
ec10117179 Merge pull request #102 from ajayyy/testing
Update master
2020-07-25 21:45:04 -04:00
Ajay Ramachandran
77f856a052 Merge pull request #101 from ajayyy/experimental
Vote changes
2020-07-25 21:43:27 -04:00
Ajay Ramachandran
15fb572b80 Added IP test and potentially improved performance 2020-07-25 21:40:49 -04:00
Ajay Ramachandran
e71680c8e3 Block voting without submitting and on the same IP 2020-07-25 21:35:43 -04:00
Ajay Ramachandran
005e88d169 Merge pull request #92 from Joe-Dowd/mysql-conv
Added ability to query a mysql database
2020-07-25 19:01:57 -04:00
Ajay Ramachandran
84c5a76e90 Merge pull request #100 from ajayyy/experimental
Fix weight not updating properly
2020-07-23 23:36:04 -04:00
Ajay Ramachandran
b6dd109fcb Fix weight not properly 2020-07-23 23:34:31 -04:00
Ajay Ramachandran
0dfe4b1ae2 Merge pull request #99 from ajayyy/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-23 19:00:00 -04:00
dependabot[bot]
c9e7ec07f5 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-23 22:14:14 +00:00
Ajay Ramachandran
369ebf48b2 Merge pull request #98 from MRuy/master
Fixed mixed up category stats
2020-07-23 18:13:42 -04:00
Nanobyte
c2e0c1be95 Fixed mixed up category stats 2020-07-17 03:24:53 +02:00
Ajay Ramachandran
4ab827da06 Merge pull request #96 from ajayyy/testing
Update master
2020-07-16 20:25:25 -04:00
Ajay Ramachandran
b965e5bf56 Merge pull request #95 from MRuy/master
Added  categoryStats for getTopUsers
2020-07-16 18:57:28 -04:00
Nanobyte
b41c633a54 Simplified categoryStats query parameter 2020-07-17 00:51:37 +02:00
Nanobyte
1823305a5e Fixed empty array for categoryStats in getTopUsers 2020-07-16 21:53:09 +02:00
Nanobyte
c4f9515c09 Added categoryStats for getTopUsers 2020-07-14 18:15:23 +02:00
Joe-Dowd
de81b135f4 removed un-needed import 2020-07-10 01:48:21 +01:00
Joe-Dowd
0d773a9a61 removed docker filed (not needed for PR) 2020-07-10 01:47:46 +01:00
Joe-Dowd
13e4adf18f PR comments resolved 2020-07-10 01:46:09 +01:00
Ajay Ramachandran
da9ab111bc Merge pull request #94 from ajayyy/testing
Update master
2020-07-08 23:04:24 -04:00
Ajay Ramachandran
adbb176697 Merge pull request #93 from ajayyy/experimental
Remove extra power downvote
2020-07-08 23:02:30 -04:00
Ajay Ramachandran
347f2ab1e1 Remove extra power downvote 2020-07-08 22:46:32 -04:00
Joe-Dowd
8e89d2bf92 Added default config for docker 2020-07-05 09:25:57 +01:00
Joe-Dowd
9a168aae45 change mount point for config 2020-07-05 05:09:06 +01:00
Joe-Dowd
b03daa3c71 executable entrypoint 2020-07-05 04:40:56 +01:00
Joe-Dowd
cd875bd0f9 docker 2020-07-05 04:37:57 +01:00
Joe-Dowd
743a5d69b2 Merge branch 'testing' of github.com:Joe-Dowd/SponsorBlockServer into mysql-conv 2020-07-05 04:35:31 +01:00
Joe-Dowd
74b82ea990 Docker files 2020-07-05 04:35:06 +01:00
Joe Dowd
220f562242 Merge pull request #1 from Joe-Dowd/mysql-conv
Mysql conv
2020-07-05 04:33:26 +01:00
Joe-Dowd
46dd16caf9 added ability to query mysql database 2020-07-05 04:14:15 +01:00
Ajay Ramachandran
325981b9c9 Merge pull request #91 from pdonias/rework-getSkipSegments
`getSkipSegments` rework
2020-06-08 22:58:01 -04:00
Pierre Donias
85dcb39f5e Rework of getSkipSegments 2020-06-03 16:56:40 +02:00
Ajay Ramachandran
fea5d234ae Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer 2020-06-02 19:28:25 -04:00
Ajay Ramachandran
aa4574cfde Add category to notification 2020-06-02 19:28:09 -04:00
Ajay Ramachandran
2e56228323 Merge pull request #90 from ajayyy/testing
Testing
2020-05-28 19:38:14 -04:00
Ajay Ramachandran
617f075c19 Added semi-colons and spacing 2020-05-28 18:24:02 -04:00
Ajay Ramachandran
ba512f004e sponsorStart, sponsorEnd -> startTime, endTime 2020-05-28 18:23:19 -04:00
Ajay Ramachandran
45d10f2667 Merge pull request #89 from pdonias/improve-similar-sponsors-groups
Improve generation of similar sponsors groups
2020-05-28 18:05:50 -04:00
Pierre Donias
47d532733b Improve generation of similar sponsors groups
by running a single flat loop
2020-05-28 22:36:47 +02:00
Ajay Ramachandran
aad75b9eea Merge branch 'experimental' of https://github.com/ajayyy/SponsorBlockServer 2020-05-25 22:50:39 -04:00
Ajay Ramachandran
73740bc920 Remove submission cap 2020-05-25 22:50:18 -04:00
Ajay Ramachandran
33cff4a08c Merge pull request #88 from ajayyy/testing
Raise max submissions per user to 16
2020-05-25 22:23:56 -04:00
Ajay Ramachandran
c16044ee2c Merge pull request #87 from ajayyy/experimental
Raise max submissions per user to 16
2020-05-25 22:23:12 -04:00
Ajay Ramachandran
2cbeb50927 Raise max submissions per user to 16 2020-05-25 22:22:53 -04:00
Martijn van der Ven
3167c24f75 Put hashedVideoID column before shadowHidden column 2020-05-23 21:27:21 +02:00
Martijn van der Ven
c7fd603933 Respond Not Found when a prefix is empty 2020-05-23 21:25:38 +02:00
Martijn van der Ven
c2510d302a Limit overlapping segments to just one through weighted randomness 2020-05-23 21:17:47 +02:00
Martijn van der Ven
d8203dce77 Make sure to always insert hashed video ID 2020-05-23 17:57:33 +02:00
Martijn van der Ven
803b1fa505 Add initial version of querying by hash prefix 2020-05-23 17:12:40 +02:00
Martijn van der Ven
ca46f4c9ac Enable database upgrades to run 2020-05-23 17:12:30 +02:00
Ajay Ramachandran
51f760332b Changed getIP to be all lowercase 2020-05-17 01:38:15 -04:00
Ajay Ramachandran
a2caa53664 Only sponsors must be longer than 1 second.
Also added proper error message when submission is too short.
2020-05-16 18:33:16 -04:00
Ajay Ramachandran
88a9628e7e Disable IP ratelimiting 2020-05-16 16:54:51 -04:00
Ajay Ramachandran
34a33bbda1 Fixed voting on non existent submission status in wrong spot 2020-05-16 16:41:55 -04:00
Ajay Ramachandran
261432a7fd Fix isUserTrustworthy missing from voting method 2020-05-16 16:40:18 -04:00
Ajay Ramachandran
06d1c082fb Fix issues with userID getting in voting 2020-05-16 00:42:38 -04:00
Ajay Ramachandran
7f07794ce4 Removed null coalescence operator 2020-05-16 00:15:33 -04:00
Ajay Ramachandran
dc7cd0993a Merge branch 'testing' of https://github.com/ajayyy/SponsorBlockServer 2020-05-16 00:12:55 -04:00
Ajay Ramachandran
7b31c40676 Prevent userID errors 2020-05-16 00:12:31 -04:00
Ajay Ramachandran
81e9d8ce0f Merge pull request #85 from ajayyy/testing
Make VIP submissions take priority
2020-05-16 00:09:57 -04:00
Ajay Ramachandran
0315aa67d7 make VIP submissions take priority. 2020-05-16 00:07:56 -04:00
Ajay Ramachandran
f57e73d87b Merge pull request #84 from ajayyy/testing
Testing
2020-05-15 19:18:03 -04:00
Ajay Ramachandran
3e2ab9c30a Merge pull request #80 from ajayyy/get-ip-fixes
Get IP options
2020-05-15 19:16:19 -04:00
Ajay Ramachandran
ad7d1a53d9 Added string check to getIP 2020-05-15 19:14:30 -04:00
Ajay Ramachandran
46abd002f2 Merge pull request #83 from ajayyy/new-vote-options
Fix adding VIPs
2020-05-14 18:31:18 -04:00
Ajay Ramachandran
8e7595cd34 Fix adding VIPs 2020-05-12 21:07:09 -04:00
Ajay Ramachandran
8e150c6862 Don't check NB for non-sponsors and properly reject if any submission fails the criteria for multiple segment submissions 2020-05-11 19:00:33 -04:00
Ajay Ramachandran
b619d91aab Merge pull request #82 from ajayyy/new-vote-options
Prevent crash when category-voting on invalid UUID
2020-05-11 18:10:12 -04:00
Ajay Ramachandran
0060b5e5af Prevent crash when category-voting on invalid UUID 2020-05-11 18:07:57 -04:00
Ajay Ramachandran
ada64d97a6 Merge pull request #78 from ajayyy/new-vote-options
New vote options + Auto-schema upgrade
2020-05-10 20:21:50 -04:00
Ajay Ramachandran
14c0aa1ec1 Make key in config unique 2020-05-10 20:19:04 -04:00
Ajay Ramachandran
b2705dfb2d Prevented upvotes on "dead" submissions unless they are VIP. 2020-05-10 00:08:05 -04:00
Ajay Ramachandran
8638698d61 Added schema upgrades for both DBs 2020-05-03 13:25:45 -04:00
Ajay Ramachandran
d4328f4d54 Change the version code to be the last run upgrade file. 2020-05-03 12:08:07 -04:00
Ajay Ramachandran
8eecffd3d5 Changed from version table to a key-value config table. 2020-05-03 12:01:49 -04:00
Ajay Ramachandran
bf2646978a Removed NB URL from example config 2020-05-03 11:49:04 -04:00
Ajay Ramachandran
f5eb522e39 Removed default NB server 2020-05-03 11:46:16 -04:00
Ajay Ramachandran
e449b5a436 Merge branch 'nb-mod-fetch' of https://github.com/andrewzlee/SponsorBlockServer into nb-mod-fetch 2020-04-30 23:42:14 -04:00
Ajay Ramachandran
318d816a37 Change test case to check 7s 2020-04-30 23:41:38 -04:00
Ajay Ramachandran
684000074e Fix overlap percentage wrong
Co-authored-by: Andrew Lee <leezandrew@gmail.com>
2020-04-30 23:40:04 -04:00
Ajay Ramachandran
5e252ed4af Added extra message to auto moderator 2020-04-30 21:19:37 -04:00
Ajay Ramachandran
eb936d579c Don't moderate if VIP. 2020-04-30 19:17:52 -04:00
Ajay Ramachandran
a278036f1d Added neural block mock 2020-04-30 19:13:42 -04:00
Ajay Ramachandran
0cf84612e9 Merge branch 'get-ip-fixes' of https://github.com/ajayyy/SponsorBlockServer into nb-mod-fetch
# Conflicts:
#	src/routes/postSkipSegments.js
2020-04-30 19:04:23 -04:00
Ajay Ramachandran
c2b17ea7a8 Made auto-moderate only call API once 2020-04-30 19:00:26 -04:00
Ajay Ramachandran
f4ce5618bf Merge branch 'testing' of https://github.com/ajayyy/SponsorBlockServer into nb-mod-fetch
# Conflicts:
#	src/routes/postSkipSegments.js
#	test/cases/postSkipSegments.js
2020-04-30 18:44:49 -04:00
Ajay Ramachandran
8290c9e1f4 Added new NB overlapping code 2020-04-30 18:39:03 -04:00
Ajay Ramachandran
abee010291 Renamed variables and added spacing. 2020-04-30 18:15:17 -04:00
Ajay Ramachandran
004f714924 Use response.ok 2020-04-30 18:10:27 -04:00
Andrew Lee
9fcd23d63a Let NB mod fail on any internal server error 2020-04-30 14:16:35 -07:00
Ajay Ramachandran
9a317a2c23 Added getIP options 2020-04-30 00:34:34 -04:00
Ajay Ramachandran
e303405ee0 Check before mkdir 2020-04-30 00:31:42 -04:00
Ajay Ramachandran
5c0062d9df Make directories if needed for the databases 2020-04-29 22:21:15 -04:00
Ajay Ramachandran
bd9a411106 Updated packages 2020-04-29 22:15:31 -04:00
Ajay Ramachandran
6a17e4d141 Added test cases for voting and fixed code to support them. 2020-04-29 22:15:16 -04:00
Ajay Ramachandran
ad169fd6e7 Added VIP support to category vote 2020-04-29 21:30:07 -04:00
Ajay Ramachandran
f23ead56ad Updated inserts to new database schema and removed null coalescing 2020-04-29 21:26:11 -04:00
Ajay Ramachandran
98f4d973e7 Added wrong category vote option. 2020-04-29 20:56:34 -04:00
Ajay Ramachandran
cb2b0aabf3 Merge branch 'testing' of https://github.com/ajayyy/SponsorBlockServer into new-vote-options
# Conflicts:
#	src/routes/voteOnSponsorTime.js
2020-04-29 19:58:19 -04:00
Ajay Ramachandran
b5fcdea62f Don't upgrade if read only 2020-04-29 19:40:17 -04:00
Ajay Ramachandran
0eec924c02 Made all updates required be performed at once. 2020-04-29 18:31:19 -04:00
Ajay Ramachandran
e27ef39dcf Remove unused function 2020-04-29 18:26:05 -04:00
Ajay Ramachandran
37587fd12a Merge pull request #79 from Joe-Dowd/mod-report-submission
add user history to discord notification on segment reporting
2020-04-29 17:45:56 -04:00
Joe-Dowd
d930853edf review changes 2020-04-29 22:27:38 +01:00
Joe-Dowd
e9e525ec63 fixed formatting 2020-04-28 05:18:54 +01:00
Joe-Dowd
13aee13f18 Improved phrasing of error responses \n removed debug logs 2020-04-28 05:10:55 +01:00
Joe-Dowd
f6df7e5f6f remove testing logs 2020-04-28 05:07:39 +01:00
Joe-Dowd
cfa80be4c7 add user history to discord notification on segment reporting 2020-04-28 05:04:51 +01:00
Ajay Ramachandran
cde51a3059 Only unhide submissions with normal vote 2020-04-27 23:02:29 -04:00
Ajay Ramachandran
d767f2ff6b Added schema upgrade system and started on new vote type 2020-04-27 23:01:51 -04:00
Andrew Lee
074e9fc0eb Merge branch 'nb-mod-fetch' of https://github.com/andrewzlee/SponsorBlockServer into nb-mod-fetch 2020-04-25 17:45:47 -04:00
Andrew Lee
ee75eea939 Changed interval check 2020-04-25 17:42:33 -04:00
Andrew Lee
1026f65280 Changed interval check 2020-04-25 17:38:21 -04:00
Andrew Lee
b101831a1b Extended timeout to test cases where necessary 2020-04-25 17:15:19 -04:00
Andrew Lee
ba555c0a7c Syntax fixes to pass tests. 2020-04-25 17:11:44 -04:00
Ajay Ramachandran
74169e6caf Merge pull request #77 from ajayyy/testing
Update master
2020-04-24 13:53:17 -04:00
Ajay Ramachandran
70ca58f98e Merge pull request #76 from Joe-Dowd/bugfix-0durationautomod
Bugfix 0durationautomod
2020-04-24 13:50:58 -04:00
Joe-Dowd
fd397de6b4 allow submission if duration is 0 2020-04-24 18:20:40 +01:00
Joe-Dowd
6cd672f9e6 allow submissions when duration is parsed as 0 2020-04-24 18:11:09 +01:00
Ajay Ramachandran
097e9e446e Merge pull request #75 from ajayyy/experimental
Fixed mocks not responding properly
2020-04-23 13:33:32 -04:00
Ajay Ramachandran
546ded3bdd Fixed mocks not responding properly 2020-04-23 12:48:02 -04:00
Ajay Ramachandran
c60d7ed75e Merge pull request #74 from ajayyy/experimental
Experimental
2020-04-23 12:03:08 -04:00
Ajay Ramachandran
a3b01e4eae Fix test process not dying 2020-04-23 11:58:36 -04:00
Ajay Ramachandran
818c288ba0 Set production mode based on config 2020-04-19 19:47:17 -04:00
Ajay Ramachandran
ad2fe00af0 Fixed database endpoint and getDatsSavedFormatted 2020-04-19 19:39:40 -04:00
Ajay Ramachandran
f8340d770c Fixed capitalization 2020-04-19 19:26:09 -04:00
Ajay Ramachandran
14e21a1009 Fixed wrong database being sent 2020-04-19 19:22:38 -04:00
Ajay Ramachandran
950b8ecb55 Update getDaysSavedFormatted.js 2020-04-19 19:18:24 -04:00
Ajay Ramachandran
5525462f11 Merge pull request #67 from ajayyy/testing
Merge the refactor into master
2020-04-19 18:59:00 -04:00
Andrew Lee
35fa384a5b Added automod check with NB 2020-04-19 12:10:39 -04:00
Andrew Lee
fee0c92280 Ignore .DS_Store 2020-04-18 13:07:38 -04:00
Ajay Ramachandran
7481b42499 Update README.MD 2020-04-16 20:14:13 -04:00
Ajay Ramachandran
7001fd2ef1 Merge pull request #73 from ajayyy/categories
Category fixes
2020-04-09 15:45:27 -04:00
Ajay Ramachandran
6aa4737b6e Fixed one category being empty causing a 404 2020-04-09 15:40:45 -04:00
Ajay Ramachandran
5eca6016fa Fixed getSkipSegments tests due to removal of JSON body as input 2020-04-09 15:40:27 -04:00
Ajay Ramachandran
585f2a2483 Merge pull request #72 from ajayyy/categories
Categories fixes
2020-04-09 15:33:28 -04:00
Ajay Ramachandran
26b9659140 Switched GET skipSegments to use params instead of JSON.
Body is not supported for GET requests.
2020-04-09 01:19:51 -04:00
Ajay Ramachandran
b2147d42a8 Merge pull request #71 from ajayyy/categories
Categories + Github Actions and Tests Fixes
2020-04-07 14:50:48 -04:00
Ajay Ramachandran
08a76dc420 Added back example config comment 2020-04-07 14:45:47 -04:00
Ajay Ramachandran
f9ec4e37e9 Added another test case for get 2020-04-07 02:41:20 -04:00
Ajay Ramachandran
d331ece0be Added JSON get test cases 2020-04-07 02:39:29 -04:00
Ajay Ramachandran
57cc4f698f Added get sponsor test cases 2020-04-07 02:24:58 -04:00
Ajay Ramachandran
0b4416d9eb Renamed old get method 2020-04-07 02:04:37 -04:00
Ajay Ramachandran
4c9aa11b9a Made old get function use new function 2020-04-07 02:03:02 -04:00
Ajay Ramachandran
3c9380c2a2 Added new segment getting endpoint 2020-04-07 01:54:19 -04:00
Ajay Ramachandran
ca8f571978 Removed extra logging 2020-04-06 20:30:27 -04:00
Ajay Ramachandran
70a39eeea4 Fixed duplicate check code 2020-04-06 20:29:58 -04:00
Ajay Ramachandran
329abfa617 Only delete test db if it exists 2020-04-06 20:26:01 -04:00
Ajay Ramachandran
739b59c65d Clear database before running tests 2020-04-06 20:17:49 -04:00
Ajay Ramachandran
06de0d61fa Fixed crashes from multiple submissions. 2020-04-06 20:14:50 -04:00
Ajay Ramachandran
557c6ad05a Fixed discord tests 2020-04-06 20:12:12 -04:00
Ajay Ramachandran
9807d3e9c7 Merge 2020-04-06 20:05:26 -04:00
Ajay Ramachandran
761fb7dafe Merge branch 'testing' of https://github.com/ajayyy/SponsorBlockServer into categories
# Conflicts:
#	src/routes/submitSponsorTimes.js
#	test/cases/submitSponsorTimes.js
2020-04-06 20:05:09 -04:00
Ajay Ramachandran
5b577eb148 Merge pull request #70 from Joe-Dowd/auto-mod
[WIP] Auto Mode using youtube api
2020-04-06 17:34:19 -04:00
Ajay Ramachandran
f1a7524acf Fixed getIP module import 2020-04-06 17:29:52 -04:00
Ajay Ramachandran
8eca458e38 Added DB checks to tests and fixed getIP using the wrong config 2020-04-06 17:25:20 -04:00
Ajay Ramachandran
67c608e76f Added github actions logging 2020-04-06 16:59:57 -04:00
Ajay Ramachandran
3a203d249e Added Github actions 2020-04-06 16:46:19 -04:00
Ajay Ramachandran
bd2f00d5f7 Added new submission method 2020-04-06 16:43:47 -04:00
Ajay Ramachandran
b74ca3962b Updated table to include category 2020-04-06 14:05:42 -04:00
Ajay Ramachandran
77f9618e0c Merge pull request #69 from Joe-Dowd/refactor
&& in npm run dev
2020-04-04 23:40:44 -04:00
Joe-Dowd
e5cd72657e added checks using the youtube api on segment submission 2020-04-04 21:37:17 +01:00
Joe-Dowd
98d754f605 && in npm run dev 2020-04-04 18:41:18 +01:00
Ajay Ramachandran
c2ba3d0ab3 Merge pull request #68 from Joe-Dowd/refactor
nodemon to watch for changes and reload with tests for "npm run dev"
2020-04-04 11:51:52 -04:00
Ajay Ramachandran
d682898828 Removed quotes from echo 2020-04-04 11:41:38 -04:00
Ajay Ramachandran
d94c81ed23 Fixed npm run dev to work on Windows 2020-04-04 11:33:22 -04:00
Ajay Ramachandran
73599be5b8 Improved sentence structure 2020-04-04 11:18:06 -04:00
Joe-Dowd
82ee4cca0f changed npm run dev to work in cmd 2020-04-04 01:15:04 +01:00
Joe-Dowd
870b80998e updated the readme 2020-04-04 00:19:28 +01:00
Joe-Dowd
2f9f4c2bc0 Added nodemon to watch for changed and reload with tests for "npm run dev" 2020-04-03 22:25:11 +01:00
Ajay Ramachandran
1ad0c195ad Merge pull request #66 from Joe-Dowd/refactor
Refactor
2020-04-03 16:48:38 -04:00
Joe-Dowd
cc0bfef0ad added more tests (categories prep) 2020-04-03 21:45:06 +01:00
Joe-Dowd
6f2798e8c4 merged master into testing 2020-04-02 18:54:21 +01:00
Joe-Dowd
819f338c68 Ensured anything in master is implemented 2020-04-02 18:53:02 +01:00
Joe-Dowd
5d2d5819cf added db files to .gitignore 2020-04-02 18:38:00 +01:00
Joe-Dowd
393c131104 remove duplicate videoID in test cases 2020-04-01 23:42:58 +01:00
Joe-Dowd
af149b09a9 added getSavedTimeForUser test 2020-04-01 23:39:56 +01:00
Joe-Dowd
3087e05212 added getHash import for getSavedTimeForUser 2020-04-01 23:29:37 +01:00
Ajay Ramachandran
0f7d1dd801 Merge pull request #64 from Joe-Dowd/refactor
Refactor
2020-04-01 17:25:44 -04:00
Joe-Dowd
461483ea36 changed a test message 2020-04-01 22:12:32 +01:00
Joe-Dowd
7bf43cfe9a You can insert into the test db in the test scripts 2020-04-01 22:02:59 +01:00
Joe-Dowd
30d24de4d3 added http tests 2020-04-01 21:29:13 +01:00
Joe-Dowd
e2f430dd09 removed db files from test 2020-04-01 21:08:48 +01:00
Joe-Dowd
5369d48eae Added testing 2020-04-01 21:04:04 +01:00
Joe-Dowd
1bff019a64 fixed youtube / discord calls 2020-04-01 19:28:21 +01:00
Ajay Ramachandran
60dfec3375 Merge pull request #59 from Joe-Dowd/createDBFromSchema
Create DB from schema on start if config option is set
2020-03-26 21:58:59 -04:00
Ajay Ramachandran
da47098ccd Added info to config example 2020-03-26 21:58:21 -04:00
Joe-Dowd
f1ee7bb430 Removed console.log 2020-03-20 21:50:57 +00:00
Joe-Dowd
be6d75c1ff add mode condition to logger util 2020-03-20 20:14:30 +00:00
Joe-Dowd
9bf065e1f0 added request debug log without condition 2020-03-20 20:14:20 +00:00
Joe-Dowd
8543647cbc added youtube util and cors middleware 2020-03-20 20:14:14 +00:00
Joe-Dowd
5c3d18b0e2 Initial refactor (routes and utils to respective folders) 2020-03-20 20:13:55 +00:00
Ajay Ramachandran
f6826b60b0 Merge pull request #63 from Joe-Dowd/more-sponsortime-checks
Bug Fix: Don't allow Infinity to be passed into the database as a sponsor time.
2020-03-17 23:50:43 -04:00
Joe Dowd
5bf86ca7bf Don't allow Infinity to be passed into the database as a sponsor time. 2020-03-18 02:25:37 +00:00
Joe Dowd
a8cbc4fbdb Stopped initial DB from being created when missing if config option set to false. Checked for existence of schema files before executing the schema. 2020-03-10 02:10:53 +00:00
Ajay Ramachandran
67bbce0b07 Merge pull request #62 from ajayyy/experimental
Added indexes and mmap to the private db
2020-03-04 15:19:23 -05:00
Ajay Ramachandran
dd2a8b682c Added indexes and mmap to the private db 2020-03-04 15:17:50 -05:00
Ajay Ramachandran
cfba798b89 Merge pull request #61 from ajayyy/experimental
Enable memory mapped IO
2020-03-02 17:57:27 -05:00
Ajay Ramachandran
d226c52630 Enable memory mapped IO 2020-03-02 17:56:00 -05:00
Ajay Ramachandran
202d7eb047 Update README.MD 2020-02-25 22:16:22 -05:00
Ajay Ramachandran
891e5d795f Merge pull request #60 from ajayyy/experimental
Allow duplicate votes
2020-02-21 18:46:15 -05:00
Ajay Ramachandran
744538eec6 Allow duplicate votes 2020-02-21 18:45:17 -05:00
Joe Dowd
97b2d2d561 Updated config comment to match changed condition for running schema script 2020-02-15 21:02:56 +00:00
Joe Dowd
1900d1ae52 Check config.readonly instead of config.mode as condition to creading tables from schema files 2020-02-15 20:52:33 +00:00
Ajay Ramachandran
3dfeefd174 Update README.MD 2020-02-12 20:35:56 -05:00
Joe Dowd
7acb19756b Create DB form schema on start if config option is set 2020-02-13 00:03:09 +00:00
Ajay Ramachandran
dc4c68903f Made sponsor submission work on POST. 2020-01-28 20:10:47 -05:00
Ajay Ramachandran
2cfe8964f3 Updated API Url 2020-01-28 15:39:08 -05:00
Ajay Ramachandran
402be52681 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental 2020-01-25 12:07:39 -05:00
Ajay Ramachandran
8f12cc527b Fixed missing get 2020-01-25 12:07:20 -05:00
Ajay Ramachandran
0a78310fdd Merge pull request #57 from ajayyy/experimental
API user count upgrade
2020-01-25 11:58:51 -05:00
Ajay Ramachandran
012f3e695e Merge pull request #56 from ajayyy/better-sqlite
Switched to Better-Sqlite
2020-01-25 11:58:25 -05:00
Ajay Ramachandran
aad16e2359 API now returns the user count from the download stores. 2020-01-24 22:56:47 -05:00
Ajay Ramachandran
d3998f5d13 Fixed shield user count 2020-01-24 21:56:57 -05:00
Ajay Ramachandran
0b6d2381dc Merge branch 'better-sqlite' of https://github.com/ajayyy/SponsorBlockServer into experimental 2020-01-24 21:51:45 -05:00
Ajay Ramachandran
b5070cf647 Switched to better sqlite 2020-01-24 21:37:16 -05:00
Ajay Ramachandran
97a2aa5015 Merge pull request #55 from ajayyy/experimental
Added WAL mode checkpoint number
2020-01-23 15:05:51 -05:00
Ajay Ramachandran
938cff5381 Now sets the journal mode to wal 2020-01-23 15:02:55 -05:00
Ajay Ramachandran
ca5890e6e3 Added WAL mode checkpoint number 2020-01-23 15:00:20 -05:00
Ajay Ramachandran
b5e569a80d Merge pull request #54 from ajayyy/experimental
Increased VIP power even more
2019-12-29 21:56:58 -05:00
Ajay Ramachandran
e868fb4c22 Increased VIP power even more. 2019-12-29 21:55:48 -05:00
Ajay Ramachandran
16d3404476 Merge branch 'experimental' of https://github.com/ajayyy/SponsorBlockServer 2019-12-29 00:25:06 -05:00
Ajay Ramachandran
f156b21ead Updated gitignore 2019-12-29 00:24:51 -05:00
Ajay Ramachandran
daef6ab06b Removed accidental upload 2019-12-29 00:24:19 -05:00
Ajay Ramachandran
2f92efc44b Merge pull request #53 from ajayyy/experimental
Added post method for view counting and vote endpoints
2019-12-29 00:23:25 -05:00
Ajay Ramachandran
94bb1665a9 Fixed formatting. 2019-12-29 00:22:39 -05:00
Ajay Ramachandran
1d1c30a483 Added post for view counting and vote endpoints. 2019-12-28 23:55:19 -05:00
Ajay Ramachandran
306456c3c7 Merge pull request #52 from ajayyy/experimental
Prevented crash from thumbnail being missing
2019-12-28 23:30:45 -05:00
Ajay Ramachandran
bf56d5c3d7 Prevented crash from thumbnail being missing. 2019-12-28 23:30:10 -05:00
Ajay Ramachandran
6e6795a6f4 Merge pull request #51 from ajayyy/experimental
Prevented overdownvoting
2019-12-28 23:26:32 -05:00
Ajay Ramachandran
befb3f69bd Prevented overdownvoting 2019-12-28 23:25:57 -05:00
Ajay Ramachandran
65d90495eb Merge pull request #50 from ajayyy/experimental
Fixed discord error
2019-12-28 13:41:31 -05:00
Ajay Ramachandran
8f9d991a6c Fixed discord error. 2019-12-28 13:41:13 -05:00
Ajay Ramachandran
fd6f6d6c9a Merge pull request #49 from ajayyy/experimental
Made it show votes before and after in the discord message
2019-12-28 11:53:40 -05:00
Ajay Ramachandran
20fbb84118 Made it show votes before and after in the discord message. 2019-12-28 11:53:25 -05:00
Ajay Ramachandran
85fd0254e8 Merge pull request #48 from ajayyy/experimental
Made the discord notification show votes after vote happened
2019-12-28 11:51:43 -05:00
Ajay Ramachandran
1b4c38d82b Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental 2019-12-28 11:50:03 -05:00
Ajay Ramachandran
c20649e14a Made the discord notification show votes after vote happened. 2019-12-28 11:49:51 -05:00
Ajay Ramachandran
e20910c920 Merge pull request #47 from ajayyy/experimental
Discord messages + Increased downvote power
2019-12-28 11:30:39 -05:00
Ajay Ramachandran
efc32f8e3a Improved VIP downvote calculation. 2019-12-28 00:48:11 -05:00
Ajay Ramachandran
d5a679731c Removed notification for first time submission if there is a conflict. 2019-12-28 00:41:33 -05:00
Ajay Ramachandran
95ccaf9478 Increased downvote power and VIP downvote power. 2019-12-28 00:38:22 -05:00
Ajay Ramachandran
50bc218ccd Fixed double downvote issue. 2019-12-28 00:36:03 -05:00
Ajay Ramachandran
765755a041 Added first time submission notifications for discord.
Also updated the downvote notification.
2019-12-28 00:29:14 -05:00
Ajay Ramachandran
54455bf625 Made errors respond 502 instead of 409. 2019-12-24 11:05:11 -05:00
Ajay Ramachandran
69f1bff0a1 Made it send a discord message when someone downvotes a sponsor. 2019-12-24 10:53:20 -05:00
Ajay Ramachandran
96fff242f4 Moved API Docs 2019-12-23 21:39:28 -05:00
Ajay Ramachandran
ae9d840567 Merge pull request #45 from ajayyy/experimental
Revert "Revert "Revert "Added all db write methods to a queue."""
2019-12-10 19:55:25 -05:00
Ajay Ramachandran
b65cab0508 Revert "Revert "Revert "Added all db write methods to a queue."""
This reverts commit 379bbee4ab.
2019-12-10 19:54:56 -05:00
Ajay Ramachandran
d0d62faf57 Merge pull request #44 from ajayyy/experimental
Revert "Revert "Added all db write methods to a queue.""
2019-12-10 19:23:14 -05:00
Ajay Ramachandran
379bbee4ab Revert "Revert "Added all db write methods to a queue.""
This reverts commit 9cf4a7ed55.
2019-12-10 19:22:35 -05:00
Ajay Ramachandran
82e3c8395c Clarified difference between local userID and public userID 2019-12-08 19:09:02 -05:00
Ajay Ramachandran
95016dab4d Fixed copying error 2019-12-08 19:04:55 -05:00
Ajay Ramachandran
9da0d47208 Update README.MD 2019-12-08 18:16:09 -05:00
Ajay Ramachandran
667b87cd92 Update README.MD 2019-12-06 21:47:44 -05:00
Ajay Ramachandran
3a9971ce8b Merge pull request #42 from ajayyy/experimental
Merged same username users
2019-12-03 17:33:08 -05:00
Ajay Ramachandran
691d87d4ff Merged users with the same username for leaderboard purposes. 2019-12-03 17:32:03 -05:00
Ajay Ramachandran
505f2c7385 Fixed getSavedTimeForUser to ignore low voted or shadow hidden submissions. 2019-12-03 17:02:02 -05:00
Ajay Ramachandran
c9988f1144 Fixed typo 2019-11-28 18:58:19 -05:00
Ajay Ramachandran
57ada81c62 Merge pull request #41 from ajayyy/experimental
Revert "Added all db write methods to a queue."
2019-11-23 01:29:59 -05:00
Ajay Ramachandran
9cf4a7ed55 Revert "Added all db write methods to a queue."
This reverts commit 17f7e618ec.
2019-11-23 01:27:32 -05:00
Ajay Ramachandran
f798fa1af1 Merge pull request #40 from ajayyy/experimental
Removed banned users from the leaderboard
2019-11-21 13:37:03 -05:00
Ajay Ramachandran
2a25e4a2d9 Removed banned users from total stats. 2019-11-21 13:27:09 -05:00
Ajay Ramachandran
aae0a6f9d4 Removed shadow hidden submissions from stats. 2019-11-15 16:16:42 -05:00
Ajay Ramachandran
145cb771b7 Merge pull request #38 from ajayyy/experimental
Performance related features
2019-10-29 17:30:28 -04:00
Ajay Ramachandran
17f7e618ec Added all db write methods to a queue.
Should resolve DB busy crash.
2019-10-29 17:29:31 -04:00
Ajay Ramachandran
4b47769f61 Added ability to enable read only mode from the config. 2019-10-28 16:37:08 -04:00
Ajay Ramachandran
e87f804070 Fixed config defined too late 2019-10-27 18:02:12 -04:00
Ajay Ramachandran
7b413dcbb2 Made db paths use the config 2019-10-27 17:57:53 -04:00
Ajay Ramachandran
ff7a12b323 Merge pull request #37 from ajayyy/experimental
Changed similar sponsor check to check if equal as well
2019-10-23 21:12:53 -04:00
Ajay Ramachandran
9cdf0596c0 Changed similar sponsor check to check if equal as well. 2019-10-23 20:15:47 -04:00
Ajay Ramachandran
909ad68444 Merge pull request #36 from ajayyy/experimental
Prevented db errors from crashing the server
2019-10-22 18:18:40 -04:00
Ajay Ramachandran
0404dfd53f Prevented db errors from crashing the server. 2019-10-22 18:18:15 -04:00
Ajay Ramachandran
ddd5269122 Merge pull request #35 from ajayyy/experimental
Fixed nothing returning when more than 4 sponsors + raised cap
2019-10-20 21:54:35 -04:00
Ajay Ramachandran
ffa70d6762 Raised cap to 8 sponsors. 2019-10-20 21:53:36 -04:00
Ajay Ramachandran
a6821209d1 Fixed weighted random cap not working.
Nothing would return if there were more than 4 sponsors.
2019-10-20 21:52:25 -04:00
Ajay Ramachandran
55a56c3e04 Removed log 2019-09-24 18:30:11 -04:00
Ajay Ramachandran
3f55bfea22 Added user based time saved endpoint. 2019-09-24 18:29:06 -04:00
Ajay Ramachandran
5346041cd1 Merge pull request #34 from ajayyy/experimental
Fixed sql schema
2019-09-16 19:46:03 -04:00
Ajay Ramachandran
6ae8001b79 Fixed sql schema. 2019-09-16 19:45:39 -04:00
Ajay Ramachandran
7a6ae31864 Merge pull request #33 from ajayyy/experimental
Added new system to hide submissions from untrustworthy users
2019-09-16 17:08:48 -04:00
Ajay Ramachandran
70bab877fb Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental 2019-09-16 17:08:00 -04:00
Ajay Ramachandran
d0757ec17b Made shadow hidden submissions count as downvoted submissions. 2019-09-16 17:07:33 -04:00
Ajay Ramachandran
fa58e786d2 Added trustworthy check function and made it possible to get unshadowbanned. 2019-09-16 16:51:55 -04:00
Ajay Ramachandran
ef5c6fddec Changed shadowHidden to 1 instead of true in the new function. 2019-09-11 15:00:35 -04:00
Ajay Ramachandran
1b53b3c993 Made untrustworthy users have hidden submissions. 2019-09-11 14:57:03 -04:00
Ajay Ramachandran
c4c6a07aeb Added install instructions 2019-09-10 23:31:16 -04:00
Ajay Ramachandran
d922de564d Added new admin endpoints 2019-09-10 23:28:15 -04:00
Ajay Ramachandran
662dcf430a Changed endpoint to post for shadowBanUser. 2019-09-10 23:18:57 -04:00
Ajay Ramachandran
63eb6cd110 Merge pull request #32 from ajayyy/experimental
Added VIP
2019-09-10 17:12:19 -04:00
Ajay Ramachandran
c924be4dc9 Changed shadow ban user endpoint to use new naming scheme 2019-09-04 22:07:23 -04:00
Ajay Ramachandran
991f155aca Made it so that VIP users start with votes after a submission. 2019-09-04 21:39:21 -04:00
Ajay Ramachandran
9ed48b1bfc Made VIP downvote count for more.
Also made it save when there is a "super downvote" to properly reverse it later.
2019-09-04 21:34:39 -04:00
Ajay Ramachandran
83c3a35ce4 Added vip addition endpoint. 2019-09-04 14:44:55 -04:00
Ajay Ramachandran
d130af130b Merge pull request #30 from ajayyy/experimental
Added port to config
2019-09-04 13:41:25 -04:00
Ajay Ramachandran
ff0b661f26 Added port to config 2019-09-04 13:41:07 -04:00
Ajay Ramachandran
62095b2847 Merge pull request #29 from ajayyy/experimental
Admin can change usernames + Config file
2019-09-04 13:21:06 -04:00
Ajay Ramachandran
347ae87b12 Added config file. 2019-09-04 13:18:47 -04:00
Ajay Ramachandran
b4c3edcd59 Allowed set username to be used by the admin to change any username. 2019-09-03 19:27:02 -04:00
Ajay Ramachandran
50b5f8ca21 Merge pull request #28 from ajayyy/experimental
Added days saved stat
2019-09-01 22:36:51 -04:00
Ajay Ramachandran
705a3c39da Added days saved stat. 2019-09-01 22:36:20 -04:00
Ajay Ramachandran
6750a5e875 Merge pull request #26 from ajayyy/experimental
ShadowHide + Sql Schemas
2019-08-24 17:06:23 -04:00
Ajay Ramachandran
cd29ce1ca3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into experimental
# Conflicts:
#	index.js
2019-08-24 17:05:10 -04:00
Ajay Ramachandran
d79921f2d6 Added shadow banning and unshadow banning users. 2019-08-24 16:24:39 -04:00
Ajay Ramachandran
f5d820c511 Merge pull request #24 from bershanskiy/master
Fix documentation
2019-08-23 14:54:30 -04:00
Ajay Ramachandran
1b4767cd38 Locked the username for undefined. 2019-08-22 16:15:13 -04:00
Ajay Ramachandran
58d3699a06 Fixed includes check 2019-08-22 14:18:31 -04:00
Ajay Ramachandran
49af7dd65d Added new shadowHidden variable that only lets it get sent out to submitters. 2019-08-22 00:01:27 -04:00
Ajay Ramachandran
4ceb7f3b47 Removed unneeded table from schema 2019-08-21 17:06:22 -04:00
Ajay Ramachandran
a6b166588f Added sql schemas 2019-08-21 17:05:08 -04:00
Anton Bershanskiy
1b4ae3ab3c Fix documentation
In actual implementation /api/getUsername is using GET.
2019-08-21 15:54:42 -05:00
Ajay Ramachandran
8c0321f2cc Merge pull request #22 from ajayyy/add-license-1
Create LICENSE
2019-08-19 20:59:21 -04:00
Ajay Ramachandran
a3f60477ec Create LICENSE 2019-08-19 20:59:10 -04:00
Ajay Ramachandran
69258587bc Raised stats limit to 100. 2019-08-19 20:42:25 -04:00
Ajay Ramachandran
9742129547 Merge pull request #21 from ajayyy/experimental
Fixed duplicate vote check being broken
2019-08-19 15:53:50 -04:00
Ajay Ramachandran
db1a3a4062 Fixed duplicate vote check being broken. 2019-08-19 15:53:31 -04:00
Ajay Ramachandran
3007c8db0e Merge pull request #20 from ajayyy/experimental
Made downvote more powerful if there are some views or votes already
2019-08-18 17:57:32 -04:00
Ajay Ramachandran
a0bbffaf91 Made downvote more powerful if there are some views or votes already. 2019-08-18 17:57:13 -04:00
Ajay Ramachandran
a984d75638 Update README.MD 2019-08-13 00:29:54 -04:00
Ajay Ramachandran
9b9638cc6e Update README.MD 2019-08-13 00:29:36 -04:00
Ajay Ramachandran
3091d86186 Update README.MD 2019-08-13 00:28:14 -04:00
Ajay Ramachandran
0072cdb17b Merge pull request #18 from ajayyy/experimental
Username support
2019-08-12 23:43:17 -04:00
Ajay Ramachandran
6b88719cf6 Reverted postVideoSponsors back to get method 2019-08-12 23:43:05 -04:00
Ajay Ramachandran
4079419fa8 Added API endpoint to get the username 2019-08-12 23:31:44 -04:00
Ajay Ramachandran
dc33bc33d6 Added ability to set a username that is used when returning the stats. 2019-08-12 23:20:56 -04:00
Ajay Ramachandran
d94c2bdf95 Removed unnecessary comment 2019-08-12 13:19:56 -04:00
Ajay Ramachandran
43f658f5e9 Merge pull request #17 from ajayyy/experimental
Changed limits and better stats
2019-08-12 13:17:20 -04:00
Ajay Ramachandran
db4ddb0b8b Made low voted submissions not count in the stats 2019-08-12 12:42:27 -04:00
Ajay Ramachandran
073717cd1f Raised per user sponsor limit to 8 2019-08-12 12:34:51 -04:00
Ajay Ramachandran
29cb68ac31 Update README.MD 2019-08-03 22:43:14 -04:00
Ajay Ramachandran
b53495a0d2 Update README.MD 2019-08-03 22:41:46 -04:00
Ajay Ramachandran
363cc1da69 Update README.MD 2019-08-03 22:41:15 -04:00
Ajay Ramachandran
3d72a674e6 Update README.MD 2019-08-03 22:40:49 -04:00
Ajay Ramachandran
06f160d8ab Added API docs 2019-08-03 22:37:35 -04:00
Ajay Ramachandran
8c235f6fcc Merge pull request #15 from ajayyy/experimental
Added user count to stats
2019-08-03 15:10:19 -04:00
Ajay Ramachandran
6df7eed22a Added user count to stats 2019-08-03 15:10:04 -04:00
Ajay Ramachandran
6f07fbc536 Merge pull request #14 from ajayyy/experimental
Added totals api endpoint
2019-08-03 12:05:19 -04:00
Ajay Ramachandran
463a48f33a Added totals api endpoint 2019-08-03 12:04:22 -04:00
Ajay Ramachandran
f449d05a38 Merge pull request #13 from ajayyy/experimental
Raised stats limit to 50
2019-08-03 00:24:11 -04:00
Ajay Ramachandran
580a9d9eba Raised stats limit to 50 2019-08-03 00:23:43 -04:00
Ajay Ramachandran
094a2fb2a0 Merge pull request #12 from ajayyy/experimental
Added stats endpoint
2019-08-03 00:13:52 -04:00
Ajay Ramachandran
4dca4081c1 Added api endpoint to get the top users 2019-08-03 00:13:21 -04:00
Ajay Ramachandran
c9ccc409a3 Merge pull request #10 from OfficialNoob/patch-1
Added hash function and BehindProxy bool
2019-07-31 23:36:12 -04:00
Ajay Ramachandran
d5d33f0e9b Reformatted and fixed missing parameters. 2019-07-31 23:32:25 -04:00
Ajay Ramachandran
dfd8d84e85 Merge pull request #11 from ajayyy/experimental
Raised cutoff due to low amount of users
2019-07-30 19:32:28 -04:00
Ajay Ramachandran
f5794f1fc3 Raised cutoff due to low amount of users. 2019-07-30 19:31:56 -04:00
Official Noob
c67fb34588 Removed uuidv1 and added GetIP() 2019-07-30 18:43:23 +01:00
Official Noob
af1ae4346f Added hash function and BehindProxy bool 2019-07-30 18:14:25 +01:00
81 changed files with 8141 additions and 880 deletions

18
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: CI
on: [push, pull_request]
jobs:
build:
name: Run Tests
runs-on: ubuntu-latest
steps:
# Initialization
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- run: npm install
- name: Run Tests
run: npm test

16
.gitignore vendored
View File

@@ -88,4 +88,18 @@ typings/
.dynamodb/
# Databases
databases
databases/sponsorTimes.db
databases/sponsorTimes.db-shm
databases/sponsorTimes.db-wal
databases/private.db
databases/sponsorTimesReal.db
test/databases/sponsorTimes.db
test/databases/sponsorTimes.db-shm
test/databases/sponsorTimes.db-wal
test/databases/private.db
# Config files
config.json
# Mac files
.DS_Store

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:12
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY index.js .
COPY src src
RUN mkdir databases
COPY databases/*.sql databases/
COPY entrypoint.sh .
EXPOSE 8080
CMD ./entrypoint.sh

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Ajay Ramachandran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -8,10 +8,30 @@ This is the server backend for it
This is a simple Sqlite database that will hold all the timing data.
To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database.db. So, you can download a backup or get archive.org to take a backup if you do desire.
To make sure that this project doesn't die, I have made the database publicly downloadable at https://sponsor.ajay.app/database.db. You can download a backup or get archive.org to take a backup if you do desire. The database is under [this license](https://creativecommons.org/licenses/by-nc-sa/4.0/) unless you get explicit permission from me.
Hopefully this project can be combined with projects like [this](https://github.com/Sponsoff/sponsorship_remover) and use this data to create a neural network to predict when sponsored segments happen. That project is sadly abandoned now, so I have decided to attempt to revive this idea.
# Client
The client web browser extension is available here: https://github.com/ajayyy/SponsorBlock
# Build Yourself
This is a node.js server, so clone this repo and run `npm install` to install all dependencies.
Make sure to put the database files in the `./databases` folder if you want to use a pre-existing database. Otherwise, a fresh database will be created.
Rename `config.json.example` to `config.json` and fill the parameters inside. Make sure to remove the comments as comments are not supported in JSON.
Ensure all the tests pass with `npm test`
Run the server with `npm start`.
# Developing
If you want to make changes, run `npm run dev` to automatically reload the server and run tests whenever a file is saved.
# API Docs
Available [here](https://github.com/ajayyy/SponsorBlock/wiki/API-Docs)

42
config.json.example Normal file
View File

@@ -0,0 +1,42 @@
{
// Remove all comments from the config when using it!
"port": 80,
"globalSalt": "[global salt (pepper) that is added to every ip before hashing to make it even harder for someone to decode the ip]",
"adminUserID": "[the hashed id of the user who can perform admin actions]",
"youtubeAPIKey": null, //get this from Google Cloud Platform [optional]
"discordReportChannelWebhookURL": null, //URL from discord if you would like notifications when someone makes a report [optional]
"discordFirstTimeSubmissionsWebhookURL": null, //URL from discord if you would like notifications when someone makes a first time submission [optional]
"discordCompletelyIncorrectReportWebhookURL": null, //URL from discord if you would like notifications when someone reports a submission as completely incorrect [optional]
"neuralBlockURL": null, // URL to check submissions against neural block. Ex. https://ai.neuralblock.app
"discordNeuralBlockRejectWebhookURL": null, //URL from discord if you would like notifications when NeuralBlock rejects a submission [optional]
"userCounterURL": null, // For user counting. URL to instance of https://github.com/ajayyy/PrivacyUserCount
"proxySubmission": null, // Base url to proxy submissions to persist // e.g. https://sponsor.ajay.app (no trailing slash)
"behindProxy": "X-Forwarded-For", //Options: "X-Forwarded-For", "Cloudflare", "X-Real-IP", anything else will mean it is not behind a proxy. True defaults to "X-Forwarded-For"
"db": "./databases/sponsorTimes.db",
"privateDB": "./databases/private.db",
"createDatabaseIfNotExist": true, //This will run on startup every time (unless readOnly is true) - so ensure "create table if not exists" is used in the schema
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "development",
"readOnly": false,
"webhooks": [],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"], // List of supported categories any other category will be rejected
"getTopUsersCacheTimeMinutes": 5, // cacheTime for getTopUsers result in minutes
"maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire
"hoursAfterWarningExpire": 24,
"rateLimit": {
"vote": {
"windowMs": 900000, // 15 minutes
"max": 20, // 20 requests in 15min time window
"message": "Too many votes, please try again later",
"statusCode": 429
},
"view": {
"windowMs": 900000, // 15 minutes
"max": 20, // 20 requests in 15min time window
"statusCode": 200
}
}
}

35
databases/_private.db.sql Normal file
View File

@@ -0,0 +1,35 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "shadowBannedUsers" (
"userID" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "votes" (
"UUID" TEXT NOT NULL,
"userID" TEXT NOT NULL,
"hashedIP" TEXT NOT NULL,
"type" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "categoryVotes" (
"UUID" TEXT NOT NULL,
"userID" TEXT NOT NULL,
"hashedIP" TEXT NOT NULL,
"category" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "sponsorTimes" (
"videoID" TEXT NOT NULL,
"hashedIP" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "config" (
"key" TEXT NOT NULL,
"value" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS sponsorTimes_hashedIP on sponsorTimes(hashedIP);
CREATE INDEX IF NOT EXISTS votes_userID on votes(UUID);
COMMIT;

View File

@@ -0,0 +1,36 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "vipUsers" (
"userID" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "sponsorTimes" (
"videoID" TEXT NOT NULL,
"startTime" REAL NOT NULL,
"endTime" REAL NOT NULL,
"votes" INTEGER NOT NULL,
"UUID" TEXT NOT NULL UNIQUE,
"userID" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL,
"views" INTEGER NOT NULL,
"category" TEXT NOT NULL,
"shadowHidden" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "userNames" (
"userID" TEXT NOT NULL,
"userName" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "categoryVotes" (
"UUID" TEXT NOT NULL,
"category" TEXT NOT NULL,
"votes" INTEGER NOT NULL default '0'
);
CREATE TABLE IF NOT EXISTS "config" (
"key" TEXT NOT NULL UNIQUE,
"value" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS sponsorTimes_videoID on sponsorTimes(videoID);
CREATE INDEX IF NOT EXISTS sponsorTimes_UUID on sponsorTimes(UUID);
COMMIT;

View File

@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
/* for testing the db upgrade, don't remove because it looks empty */
/* Add version to config */
INSERT INTO config (key, value) VALUES("version", 1);
COMMIT;

View File

@@ -0,0 +1,25 @@
BEGIN TRANSACTION;
/* Add incorrectVotes field */
CREATE TABLE "sqlb_temp_table_1" (
"videoID" TEXT NOT NULL,
"startTime" REAL NOT NULL,
"endTime" REAL NOT NULL,
"votes" INTEGER NOT NULL,
"incorrectVotes" INTEGER NOT NULL default '1',
"UUID" TEXT NOT NULL UNIQUE,
"userID" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL,
"views" INTEGER NOT NULL,
"category" TEXT NOT NULL DEFAULT "sponsor",
"shadowHidden" INTEGER NOT NULL
);
INSERT INTO sqlb_temp_table_1 SELECT videoID,startTime,endTime,votes,"1",UUID,userID,timeSubmitted,views,category,shadowHidden FROM sponsorTimes;
DROP TABLE sponsorTimes;
ALTER TABLE sqlb_temp_table_1 RENAME TO "sponsorTimes";
/* Add version to config */
INSERT INTO config (key, value) VALUES("version", 1);
COMMIT;

View File

@@ -0,0 +1,13 @@
BEGIN TRANSACTION;
/* Add new table: noSegments */
CREATE TABLE "noSegments" (
"videoID" TEXT NOT NULL,
"userID" TEXT NOT NULL,
"category" TEXT NOT NULL
);
/* Add version to config */
UPDATE config SET value = 2 WHERE key = 'version';
COMMIT;

View File

@@ -0,0 +1,13 @@
BEGIN TRANSACTION;
/* hash upgrade test sha256('vid') = '1ff838dc6ca9680d88455341118157d59a055fe6d0e3870f9c002847bebe4663'
/* Add hash field */
ALTER TABLE sponsorTimes ADD hashedVideoID TEXT NOT NULL default "";
UPDATE sponsorTimes SET hashedVideoID = sha256(videoID);
CREATE INDEX IF NOT EXISTS sponsorTimes_hashedVideoID on sponsorTimes(hashedVideoID);
/* Bump version in config */
UPDATE config SET value = 3 WHERE key = "version";
COMMIT;

View File

@@ -0,0 +1,12 @@
BEGIN TRANSACTION;
/* Create warnings table */
CREATE TABLE "warnings" (
userID TEXT NOT NULL,
issueTime INTEGER NOT NULL,
issuerUserID TEXT NOT NULL
);
UPDATE config SET value = 4 WHERE key = "version";
COMMIT;

26
entrypoint.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e
echo 'Entrypoint script'
cd /usr/src/app
cp /etc/sponsorblock/config.json . || cat <<EOF > config.json
{
"port": 8080,
"globalSalt": "[CHANGE THIS]",
"adminUserID": "[CHANGE THIS]",
"youtubeAPIKey": null,
"discordReportChannelWebhookURL": null,
"discordFirstTimeSubmissionsWebhookURL": null,
"discordAutoModWebhookURL": null,
"proxySubmission": null,
"behindProxy": "X-Forwarded-For",
"db": "./databases/sponsorTimes.db",
"privateDB": "./databases/private.db",
"createDatabaseIfNotExist": true,
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "development",
"readOnly": false
}
EOF
node index.js

501
index.js
View File

@@ -1,497 +1,6 @@
var express = require('express');
var http = require('http');
// Create a service (the app object is just a callback).
var app = express();
//uuid service
var uuidv1 = require('uuid/v1');
//hashing service
var crypto = require('crypto');
//load database
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('./databases/sponsorTimes.db');
//where the more sensitive data such as IP addresses are stored
var privateDB = new sqlite3.Database('./databases/private.db');
// Create an HTTP service.
http.createServer(app).listen(80);
//global salt that is added to every ip before hashing to
// make it even harder for someone to decode the ip
var globalSalt = "49cb0d52-1aec-4b89-85fc-fab2c53062fb";
//setup CORS correctly
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
var config = require('./src/config.js');
var createServer = require('./src/app.js');
const logger = require('./src/utils/logger.js');
var server = createServer(() => {
logger.info("Server started on port " + config.port + ".");
});
//add the get function
app.get('/api/getVideoSponsorTimes', function (req, res) {
let videoID = req.query.videoID;
let sponsorTimes = [];
let votes = []
let UUIDs = [];
db.prepare("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ? ORDER BY startTime").all(videoID, function(err, rows) {
if (err) console.log(err);
for (let i = 0; i < rows.length; i++) {
//check if votes are above -2
if (rows[i].votes < -2) {
//too untrustworthy, just ignore it
continue;
}
sponsorTimes.push([]);
let index = sponsorTimes.length - 1;
sponsorTimes[index][0] = rows[i].startTime;
sponsorTimes[index][1] = rows[i].endTime;
votes[index] = rows[i].votes;
UUIDs[index] = rows[i].UUID;
}
if (sponsorTimes.length == 0) {
res.sendStatus(404);
return;
}
organisedData = getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs);
sponsorTimes = organisedData.sponsorTimes;
UUIDs = organisedData.UUIDs;
if (sponsorTimes.length == 0) {
res.sendStatus(404);
} else {
//send result
res.send({
sponsorTimes: sponsorTimes,
UUIDs: UUIDs
})
}
});
});
//add the post function
app.get('/api/postVideoSponsorTimes', function (req, res) {
let videoID = req.query.videoID;
let startTime = req.query.startTime;
let endTime = req.query.endTime;
let userID = req.query.userID;
//check if all correct inputs are here and the length is 1 second or more
if (videoID == undefined || startTime == undefined || endTime == undefined || userID == undefined
|| Math.abs(startTime - endTime) < 1) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHashedUserID(userID);
//x-forwarded-for if this server is behind a proxy
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
//hash the ip so no one can get it from the database
let hashedIP = ip + globalSalt;
//hash it 5000 times, this makes it very hard to brute force
for (let i = 0; i < 5000; i++) {
let hashCreator = crypto.createHash('sha256');
hashedIP = hashCreator.update(hashedIP).digest('hex');
}
startTime = parseFloat(startTime);
endTime = parseFloat(endTime);
if (isNaN(startTime) || isNaN(endTime)) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime > endTime) {
//time can't go backwards
res.sendStatus(400);
return;
}
//this can just be a hash of the data
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
let hashCreator = crypto.createHash('sha256');
let UUID = hashCreator.update(videoID + startTime + endTime + userID).digest('hex');
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday], function(err, row) {
if (row.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
} else {
//check to see if the user has already submitted sponsors for this video
db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID], function(err, row) {
if (row.count >= 4) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
} else {
//check if this info has already been submitted first
db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID], function(err, row) {
if (err) console.log(err);
if (row == null) {
//not a duplicate, execute query
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, 0, UUID, userID, timeSubmitted, 0);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
res.sendStatus(200);
} else {
res.sendStatus(409);
}
});
}
});
}
});
});
//voting endpoint
app.get('/api/voteOnSponsorTime', function (req, res) {
let UUID = req.query.UUID;
let userID = req.query.userID;
let type = req.query.type;
if (UUID == undefined || userID == undefined || type == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHashedUserID(userID + UUID);
//x-forwarded-for if this server is behind a proxy
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
//hash the ip so no one can get it from the database
let hashedIP = ip + globalSalt;
//hash it 5000 times, this makes it very hard to brute force
for (let i = 0; i < 5000; i++) {
let hashCreator = crypto.createHash('sha256');
hashedIP = hashCreator.update(hashedIP).digest('hex');
}
//check if vote has already happened
privateDB.prepare("SELECT type FROM votes WHERE userID = ? AND UUID = ?").get(userID, UUID, function(err, row) {
if (err) console.log(err);
if (row != undefined && row.type == type) {
//they have already done this exact vote
res.status(405).send("Duplicate Vote");
return;
}
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
let oldIncrementAmount = 0;
if (type == 1) {
//upvote
incrementAmount = 1;
} else if (type == 0) {
//downvote
incrementAmount = -1;
} else {
//unrecongnised type of vote
res.sendStatus(400);
return;
}
if (row != undefined) {
if (row.type == 1) {
//upvote
oldIncrementAmount = 1;
} else if (row.type == 0) {
//downvote
oldIncrementAmount = -1;
}
}
//update the votes table
if (row != undefined) {
privateDB.prepare("UPDATE votes SET type = ? WHERE userID = ? AND UUID = ?").run(type, userID, UUID);
} else {
privateDB.prepare("INSERT INTO votes VALUES(?, ?, ?, ?)").run(UUID, userID, hashedIP, type);
}
//update the vote count on this sponsorTime
//oldIncrementAmount will be zero is row is null
db.prepare("UPDATE sponsorTimes SET votes = votes + ? WHERE UUID = ?").run(incrementAmount - oldIncrementAmount, UUID);
//added to db
res.sendStatus(200);
});
});
//Endpoint when a sponsorTime is used up
app.get('/api/viewedVideoSponsorTime', function (req, res) {
let UUID = req.query.UUID;
if (UUID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//up the view count by one
db.prepare("UPDATE sponsorTimes SET views = views + 1 WHERE UUID = ?").run(UUID);
res.sendStatus(200);
});
//Gets all the views added up for one userID
//Useful to see how much one user has contributed
app.get('/api/getViewsForUser', function (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHashedUserID(userID);
//up the view count by one
db.prepare("SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ?").get(userID, function(err, row) {
if (err) console.log(err);
if (row.viewCount != null) {
res.send({
viewCount: row.viewCount
});
} else {
res.sendStatus(404);
}
});
});
app.get('/database.db', function (req, res) {
res.sendFile("./databases/sponsorTimes.db", { root: __dirname });
});
function getHashedUserID(userID) {
//hash the userID so no one can get it from the database
let hashedUserID = userID;
//hash it 5000 times, this makes it very hard to brute force
for (let i = 0; i < 5000; i++) {
let hashCreator = crypto.createHash('sha256');
hashedUserID = hashCreator.update(hashedUserID).digest('hex');
}
return hashedUserID;
}
//This function will find sponsor times that are contained inside of eachother, called similar sponsor times
//Only one similar time will be returned, randomly generated based on the sqrt of votes.
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Sponsor times with less than -2 votes are already ignored before this function is called
function getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs) {
//list of sponsors that are contained inside eachother
let similarSponsors = [];
for (let i = 0; i < sponsorTimes.length; i++) {
//see if the start time is located between the start and end time of the other sponsor time.
for (let j = 0; j < sponsorTimes.length; j++) {
if (sponsorTimes[j][0] > sponsorTimes[i][0] && sponsorTimes[j][0] < sponsorTimes[i][1]) {
//sponsor j is contained in sponsor i
similarSponsors.push([i, j]);
}
}
}
let similarSponsorsGroups = [];
//once they have been added to a group, they don't need to be dealt with anymore
let dealtWithSimilarSponsors = [];
//create lists of all the similar groups (if 1 and 2 are similar, and 2 and 3 are similar, the group is 1, 2, 3)
for (let i = 0; i < similarSponsors.length; i++) {
if (dealtWithSimilarSponsors.includes(i)) {
//dealt with already
continue;
}
//this is the group of indexes that are similar
let group = similarSponsors[i];
for (let j = 0; j < similarSponsors.length; j++) {
if (group.includes(similarSponsors[j][0]) || group.includes(similarSponsors[j][1])) {
//this is a similar group
group.push(similarSponsors[j][0]);
group.push(similarSponsors[j][1]);
dealtWithSimilarSponsors.push(j);
}
}
similarSponsorsGroups.push(group);
}
//remove duplicate indexes in group arrays
for (let i = 0; i < similarSponsorsGroups.length; i++) {
uniqueArray = similarSponsorsGroups[i].filter(function(item, pos, self) {
return self.indexOf(item) == pos;
});
similarSponsorsGroups[i] = uniqueArray;
}
let weightedRandomIndexes = getWeightedRandomChoiceForArray(similarSponsorsGroups, votes);
let finalSponsorTimeIndexes = weightedRandomIndexes.finalChoices;
//the sponsor times either chosen to be added to finalSponsorTimeIndexes or chosen not to be added
let finalSponsorTimeIndexesDealtWith = weightedRandomIndexes.choicesDealtWith;
let voteSums = weightedRandomIndexes.weightSums;
//convert these into the votes
for (let i = 0; i < voteSums.length; i++) {
if (voteSums[i] != undefined) {
//it should use the sum of votes, since anyone upvoting a similar sponsor is upvoting the existence of that sponsor.
votes[finalSponsorTimeIndexes[i]] = voteSums;
}
}
//find the indexes never dealt with and add them
for (let i = 0; i < sponsorTimes.length; i++) {
if (!finalSponsorTimeIndexesDealtWith.includes(i)) {
finalSponsorTimeIndexes.push(i)
}
}
//if there are too many indexes, find the best 4
if (finalSponsorTimeIndexes.length > 4) {
finalSponsorTimeIndexes = getWeightedRandomChoice(finalSponsorTimeIndexes, votes, 4).finalChoices;
}
//convert this to a final array to return
let finalSponsorTimes = [];
for (let i = 0; i < finalSponsorTimeIndexes.length; i++) {
finalSponsorTimes.push(sponsorTimes[finalSponsorTimeIndexes[i]]);
}
//convert this to a final array of UUIDs as well
let finalUUIDs = [];
for (let i = 0; i < finalSponsorTimeIndexes.length; i++) {
finalUUIDs.push(UUIDs[finalSponsorTimeIndexes[i]]);
}
return {
sponsorTimes: finalSponsorTimes,
UUIDs: finalUUIDs
};
}
//gets the getWeightedRandomChoice for each group in an array of groups
function getWeightedRandomChoiceForArray(choiceGroups, weights) {
let finalChoices = [];
//the indexes either chosen to be added to final indexes or chosen not to be added
let choicesDealtWith = [];
//for each choice group, what are the sums of the weights
let weightSums = [];
for (let i = 0; i < choiceGroups.length; i++) {
//find weight sums for this group
weightSums.push(0);
for (let j = 0; j < choiceGroups[i].length; j++) {
//only if it is a positive vote, otherwise it is probably just a sponsor time with slightly wrong time
if (weights[choiceGroups[i][j]] > 0) {
weightSums[weightSums.length - 1] += weights[choiceGroups[i][j]];
}
}
//create a random choice for this group
let randomChoice = getWeightedRandomChoice(choiceGroups[i], weights, 1)
finalChoices.push(randomChoice.finalChoices);
for (let j = 0; j < randomChoice.choicesDealtWith.length; j++) {
choicesDealtWith.push(randomChoice.choicesDealtWith[j])
}
}
return {
finalChoices: finalChoices,
choicesDealtWith: choicesDealtWith,
weightSums: weightSums
};
}
//gets a weighted random choice from the indexes array based on the weights.
//amountOfChoices speicifies the amount of choices to return, 1 or more.
//choices are unique
function getWeightedRandomChoice(choices, weights, amountOfChoices) {
if (amountOfChoices > choices.length) {
//not possible, since all choices must be unique
return null;
}
let finalChoices = [];
let choicesDealtWith = [];
let sqrtWeightsList = [];
//the total of all the weights run through the cutom sqrt function
let totalSqrtWeights = 0;
for (let j = 0; j < choices.length; j++) {
//multiplying by 10 makes around 13 votes the point where it the votes start not mattering as much (10 + 3)
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/ljftxolg9j
//this can be changed if this system increases in popularity.
let sqrtVote = Math.sqrt((weights[choices[j]] + 3) * 10);
sqrtWeightsList.push(sqrtVote)
totalSqrtWeights += sqrtVote;
//this index has now been deat with
choicesDealtWith.push(choices[j]);
}
//iterate and find amountOfChoices choices
let randomNumber = Math.random();
//this array will keep adding to this variable each time one sqrt vote has been dealt with
//this is the sum of all the sqrtVotes under this index
let currentVoteNumber = 0;
for (let j = 0; j < sqrtWeightsList.length; j++) {
if (randomNumber > currentVoteNumber / totalSqrtWeights && randomNumber < (currentVoteNumber + sqrtWeightsList[j]) / totalSqrtWeights) {
//this one was randomly generated
finalChoices.push(choices[j]);
//remove that from original array, for next recursion pass if it happens
choices.splice(j, 1);
break;
}
//add on to the count
currentVoteNumber += sqrtWeightsList[j];
}
//add on the other choices as well using recursion
if (amountOfChoices > 1) {
let otherChoices = getWeightedRandomChoice(choices, weights, amountOfChoices - 1).finalChoices;
//add all these choices to the finalChoices array being returned
for (let i = 0; i < otherChoices.length; i++) {
finalChoices.push(otherChoices[i]);
}
}
return {
finalChoices: finalChoices,
choicesDealtWith: choicesDealtWith
};
}

2696
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,27 @@
"description": "Server that holds the SponsorBlock database",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node test.js",
"dev": "nodemon -x \"(npm test || echo test failed) && npm start\"",
"dev:bash": "nodemon -x 'npm test ; npm start'",
"start": "node index.js"
},
"author": "Ajay Ramachandran",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^5.4.3",
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"http": "0.0.0",
"sqlite3": "^4.0.9",
"uuid": "^3.3.2"
"iso8601-duration": "^1.2.0",
"node-fetch": "^2.6.0",
"redis": "^3.0.2",
"sync-mysql": "^3.0.1",
"uuid": "^3.3.2",
"youtube-api": "^2.0.10"
},
"devDependencies": {
"mocha": "^7.1.1",
"nodemon": "^2.0.2"
}
}

137
src/app.js Normal file
View File

@@ -0,0 +1,137 @@
var express = require('express');
// Create a service (the app object is just a callback).
var app = express();
var config = require('./config.js');
var redis = require('./utils/redis.js');
const getIP = require('./utils/getIP.js');
const getHash = require('./utils/getHash.js');
// Middleware
const rateLimitMiddleware = require('./middleware/requestRateLimit.js');
var corsMiddleware = require('./middleware/cors.js');
var loggerMiddleware = require('./middleware/logger.js');
const userCounter = require('./middleware/userCounter.js');
// Routes
var getSkipSegments = require('./routes/getSkipSegments.js').endpoint;
var postSkipSegments = require('./routes/postSkipSegments.js');
var getSkipSegmentsByHash = require('./routes/getSkipSegmentsByHash.js');
var voteOnSponsorTime = require('./routes/voteOnSponsorTime.js');
var viewedVideoSponsorTime = require('./routes/viewedVideoSponsorTime.js');
var setUsername = require('./routes/setUsername.js');
var getUsername = require('./routes/getUsername.js');
var shadowBanUser = require('./routes/shadowBanUser.js');
var addUserAsVIP = require('./routes/addUserAsVIP.js');
var getSavedTimeForUser = require('./routes/getSavedTimeForUser.js');
var getViewsForUser = require('./routes/getViewsForUser.js');
var getTopUsers = require('./routes/getTopUsers.js');
var getTotalStats = require('./routes/getTotalStats.js');
var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js');
var getUserInfo = require('./routes/getUserInfo.js');
var postNoSegments = require('./routes/postNoSegments.js');
var deleteNoSegments = require('./routes/deleteNoSegments.js');
var getIsUserVIP = require('./routes/getIsUserVIP.js');
var warnUser = require('./routes/postWarning.js');
var postSegmentShift = require('./routes/postSegmentShift.js');
// Old Routes
var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js');
var oldSubmitSponsorTimes = require('./routes/oldSubmitSponsorTimes.js');
// Rate limit endpoint lists
let voteEndpoints = [voteOnSponsorTime.endpoint];
let viewEndpoints = [viewedVideoSponsorTime];
if (config.rateLimit) {
if (config.rateLimit.vote) voteEndpoints.unshift(rateLimitMiddleware(config.rateLimit.vote));
if (config.rateLimit.view) viewEndpoints.unshift(rateLimitMiddleware(config.rateLimit.view));
}
//setup CORS correctly
app.use(corsMiddleware);
app.use(loggerMiddleware);
app.use(express.json())
if (config.userCounterURL) app.use(userCounter);
// Setup pretty JSON
if (config.mode === "development") app.set('json spaces', 2);
// Set production mode
app.set('env', config.mode || 'production');
//add the get function
app.get('/api/getVideoSponsorTimes', oldGetVideoSponsorTimes);
//add the oldpost function
app.get('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
app.post('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
//add the skip segments functions
app.get('/api/skipSegments', getSkipSegments);
app.post('/api/skipSegments', postSkipSegments);
// add the privacy protecting skip segments functions
app.get('/api/skipSegments/:prefix', getSkipSegmentsByHash);
//voting endpoint
app.get('/api/voteOnSponsorTime', ...voteEndpoints);
app.post('/api/voteOnSponsorTime', ...voteEndpoints);
//Endpoint when a submission is skipped
app.get('/api/viewedVideoSponsorTime', ...viewEndpoints);
app.post('/api/viewedVideoSponsorTime', ...viewEndpoints);
//To set your username for the stats view
app.post('/api/setUsername', setUsername);
//get what username this user has
app.get('/api/getUsername', getUsername);
//Endpoint used to hide a certain user's data
app.post('/api/shadowBanUser', shadowBanUser);
//Endpoint used to make a user a VIP user with special privileges
app.post('/api/addUserAsVIP', addUserAsVIP);
//Gets all the views added up for one userID
//Useful to see how much one user has contributed
app.get('/api/getViewsForUser', getViewsForUser);
//Gets all the saved time added up (views * sponsor length) for one userID
//Useful to see how much one user has contributed
//In minutes
app.get('/api/getSavedTimeForUser', getSavedTimeForUser);
app.get('/api/getTopUsers', getTopUsers);
//send out totals
//send the total submissions, total views and total minutes saved
app.get('/api/getTotalStats', getTotalStats);
app.get('/api/getUserInfo', getUserInfo);
//send out a formatted time saved total
app.get('/api/getDaysSavedFormatted', getDaysSavedFormatted);
//submit video containing no segments
app.post('/api/noSegments', postNoSegments);
app.delete('/api/noSegments', deleteNoSegments);
//get if user is a vip
app.get('/api/isUserVIP', getIsUserVIP);
//sent user a warning
app.post('/api/warnUser', warnUser);
//get if user is a vip
app.post('/api/segmentShift', postSegmentShift);
app.get('/database.db', function (req, res) {
res.sendFile("./databases/sponsorTimes.db", { root: "./" });
});
// Create an HTTP service.
module.exports = function createServer (callback) {
return app.listen(config.port, callback);
}

37
src/config.js Normal file
View File

@@ -0,0 +1,37 @@
const fs = require('fs');
let config = {};
// Check to see if launched in test mode
if (process.env.npm_lifecycle_script === 'node test.js') {
config = JSON.parse(fs.readFileSync('test.json'));
} else {
config = JSON.parse(fs.readFileSync('config.json'));
}
addDefaults(config, {
"port": 80,
"behindProxy": "X-Forwarded-For",
"db": "./databases/sponsorTimes.db",
"privateDB": "./databases/private.db",
"createDatabaseIfNotExist": true,
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"readOnly": false,
"webhooks": [],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24
})
module.exports = config;
// Add defaults
function addDefaults(config, defaults) {
for (const key in defaults) {
if(!config.hasOwnProperty(key)) {
config[key] = defaults[key];
}
}
};

28
src/databases/Mysql.js Normal file
View File

@@ -0,0 +1,28 @@
var MysqlInterface = require('sync-mysql');
const logger = require('../utils/logger.js');
class Mysql {
constructor(msConfig) {
this.connection = new MysqlInterface(msConfig);
}
exec(query) {
this.prepare('run', query, []);
}
prepare (type, query, params) {
logger.debug("prepare (mysql): type: " + type + ", query: " + query + ", params: " + params);
if (type === 'get') {
return this.connection.query(query, params)[0];
} else if (type === 'run') {
this.connection.query(query, params);
} else if (type === 'all') {
return this.connection.query(query, params);
} else {
logger.warn('returning undefined...');
return undefined;
}
}
}
module.exports = Mysql;

33
src/databases/Sqlite.js Normal file
View File

@@ -0,0 +1,33 @@
const { db } = require("./databases");
var config = require('../config.js');
const logger = require('../utils/logger.js');
class Sqlite {
constructor(connection) {
this.connection = connection;
}
getConnection() {
return this.connection;
}
prepare(type, query, params) {
if (type === 'get') {
return this.connection.prepare(query).get(...params);
} else if (type === 'run') {
this.connection.prepare(query).run(...params);
} else if (type === 'all') {
return this.connection.prepare(query).all(...params);
} else {
logger.debug('sqlite query: returning undefined')
logger.debug("prepare: type: " + type + ", query: " + query + ", params: " + params);
return undefined;
}
}
exec(query) {
return this.connection.exec(query);
}
}
module.exports = Sqlite;

View File

@@ -0,0 +1,81 @@
var config = require('../config.js');
var Sqlite3 = require('better-sqlite3');
var fs = require('fs');
var path = require('path');
var Sqlite = require('./Sqlite.js')
var Mysql = require('./Mysql.js');
const logger = require('../utils/logger.js');
const getHash = require('../utils/getHash.js');
let options = {
readonly: config.readOnly,
fileMustExist: !config.createDatabaseIfNotExist
};
if (config.mysql) {
module.exports = {
db: new Mysql(config.mysql),
privateDB: new Mysql(config.privateMysql)
};
} else {
// Make dirs if required
if (!fs.existsSync(path.join(config.db, "../"))) {
fs.mkdirSync(path.join(config.db, "../"));
}
if (!fs.existsSync(path.join(config.privateDB, "../"))) {
fs.mkdirSync(path.join(config.privateDB, "../"));
}
var db = new Sqlite3(config.db, options);
var privateDB = new Sqlite3(config.privateDB, options);
if (config.createDatabaseIfNotExist && !config.readOnly) {
if (fs.existsSync(config.dbSchema)) db.exec(fs.readFileSync(config.dbSchema).toString());
if (fs.existsSync(config.privateDBSchema)) privateDB.exec(fs.readFileSync(config.privateDBSchema).toString());
}
if (!config.readOnly) {
db.function("sha256", (string) => {
return getHash(string, 1);
});
// Upgrade database if required
ugradeDB(db, "sponsorTimes");
ugradeDB(privateDB, "private")
}
// Attach private db to main db
db.prepare("ATTACH ? as privateDB").run(config.privateDB);
// Enable WAL mode checkpoint number
if (!config.readOnly && config.mode === "production") {
db.exec("PRAGMA journal_mode=WAL;");
db.exec("PRAGMA wal_autocheckpoint=1;");
}
// Enable Memory-Mapped IO
db.exec("pragma mmap_size= 500000000;");
privateDB.exec("pragma mmap_size= 500000000;");
module.exports = {
db: new Sqlite(db),
privateDB: new Sqlite(privateDB)
};
function ugradeDB(db, prefix) {
let versionCodeInfo = db.prepare("SELECT value FROM config WHERE key = ?").get("version");
let versionCode = versionCodeInfo ? versionCodeInfo.value : 0;
let path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (parseInt(versionCode) + 1) + ".sql";
logger.debug('db update: trying ' + path);
while (fs.existsSync(path)) {
logger.debug('db update: updating ' + path);
db.exec(fs.readFileSync(path).toString());
versionCode = db.prepare("SELECT value FROM config WHERE key = ?").get("version").value;
path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (parseInt(versionCode) + 1) + ".sql";
logger.debug('db update: trying ' + path);
}
logger.debug('db update: no file ' + path);
}
}

5
src/middleware/cors.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = function corsMiddleware(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
}

6
src/middleware/logger.js Normal file
View File

@@ -0,0 +1,6 @@
const log = require('../utils/logger.js'); // log not logger to not interfere with function name
module.exports = function logger (req, res, next) {
log.info("Request recieved: " + req.method + " " + req.url);
next();
}

View File

@@ -0,0 +1,18 @@
const getIP = require('../utils/getIP.js');
const getHash = require('../utils/getHash.js');
const rateLimit = require('express-rate-limit');
module.exports = (limitConfig) => rateLimit({
windowMs: limitConfig.windowMs,
max: limitConfig.max,
message: limitConfig.message,
statusCode: limitConfig.statusCode,
headers: false,
keyGenerator: (req /*, res*/) => {
return getHash(getIP(req), 1);
},
skip: (/*req, res*/) => {
// skip rate limit if running in test mode
return process.env.npm_lifecycle_script === 'node test.js';
}
});

View File

@@ -0,0 +1,13 @@
const fetch = require('node-fetch');
const config = require('../config.js');
const getIP = require('../utils/getIP.js');
const getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = function userCounter(req, res, next) {
fetch(config.userCounterURL + "/api/v1/addIP?hashedIP=" + getHash(getIP(req), 1), { method: "POST" })
.catch(() => logger.debug("Failing to connect to user counter at: " + config.userCounterURL))
next();
}

View File

@@ -0,0 +1,45 @@
var fs = require('fs');
var config = require('../config.js');
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
module.exports = async function addUserAsVIP (req, res) {
let userID = req.query.userID;
let adminUserIDInput = req.query.adminUserID;
let enabled = req.query.enabled;
if (enabled === undefined){
enabled = true;
} else {
enabled = enabled === "true";
}
if (userID == undefined || adminUserIDInput == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
adminUserIDInput = getHash(adminUserIDInput);
if (adminUserIDInput !== config.adminUserID) {
//not authorized
res.sendStatus(403);
return;
}
//check to see if this user is already a vip
let row = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the vip list
db.prepare('run', "INSERT INTO vipUsers VALUES(?)", [userID]);
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
db.prepare('run', "DELETE FROM vipUsers WHERE userID = ?", [userID]);
}
res.sendStatus(200);
}

View File

@@ -0,0 +1,43 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
module.exports = (req, res) => {
// Collect user input data
let videoID = req.body.videoID;
let userID = req.body.userID;
let categories = req.body.categories;
// Check input data is valid
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
) {
res.status(400).json({
message: 'Bad Format'
});
return;
}
// Check if user is VIP
userID = getHash(userID);
let userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to mark videos.'
});
return;
}
db.prepare("all", 'SELECT * FROM noSegments WHERE videoID = ?', [videoID]).filter((entry) => {
return (categories.indexOf(entry.category) !== -1);
}).forEach((entry) => {
db.prepare('run', 'DELETE FROM noSegments WHERE videoID = ? AND category = ?', [videoID, entry.category]);
});
res.status(200).json({message: 'Removed no segments entrys for video ' + videoID});
};

View File

@@ -0,0 +1,12 @@
var db = require('../databases/databases.js').db;
module.exports = function getDaysSavedFormatted (req, res) {
let row = db.prepare('get', "SELECT SUM((endTime - startTime) / 60 / 60 / 24 * views) as daysSaved from sponsorTimes where shadowHidden != 1", []);
if (row !== undefined) {
//send this result
res.send({
daysSaved: row.daysSaved.toFixed(2)
});
}
}

View File

@@ -0,0 +1,31 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
const isUserVIP = require('../utils/isUserVIP.js');
module.exports = (req, res) => {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let vipState = isUserVIP(userID);
res.status(200).json({
hashedUserID: userID,
vip: vipState
});
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,32 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
module.exports = function getSavedTimeForUser (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare("get", "SELECT SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE userID = ? AND votes > -1 AND shadowHidden != 1 ", [userID]);
if (row.minutesSaved != null) {
res.send({
timeSaved: row.minutesSaved
});
} else {
res.sendStatus(404);
}
} catch (err) {
console.log(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,189 @@
var config = require('../config.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var logger = require('../utils/logger.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
function cleanGetSegments(req, videoID, categories) {
let userHashedIP, shadowHiddenSegments;
let segments = [];
try {
for (const category of categories) {
const categorySegments = db
.prepare(
'all',
'SELECT startTime, endTime, votes, UUID, shadowHidden FROM sponsorTimes WHERE videoID = ? and category = ? ORDER BY startTime',
[videoID, category]
)
.filter(segment => {
if (segment.votes < -1) {
return false; //too untrustworthy, just ignore it
}
//check if shadowHidden
//this means it is hidden to everyone but the original ip that submitted it
if (segment.shadowHidden != 1) {
return true;
}
if (shadowHiddenSegments === undefined) {
shadowHiddenSegments = privateDB.prepare('all', 'SELECT hashedIP FROM sponsorTimes WHERE videoID = ?', [videoID]);
}
//if this isn't their ip, don't send it to them
return shadowHiddenSegments.some(shadowHiddenSegment => {
if (userHashedIP === undefined) {
//hash the IP only if it's strictly necessary
userHashedIP = getHash(getIP(req) + config.globalSalt);
}
return shadowHiddenSegment.hashedIP === userHashedIP;
});
});
chooseSegments(categorySegments).forEach(chosenSegment => {
segments.push({
category,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
});
});
}
return segments;
} catch (err) {
if (err) {
logger.error(err);
return null;
}
}
}
//gets a weighted random choice from the choices array based on their `votes` property.
//amountOfChoices specifies the maximum amount of choices to return, 1 or more.
//choices are unique
function getWeightedRandomChoice(choices, amountOfChoices) {
//trivial case: no need to go through the whole process
if (amountOfChoices >= choices.length) {
return choices;
}
//assign a weight to each choice
let totalWeight = 0;
choices = choices.map(choice => {
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/c1duhfrmts
//this can be changed if this system increases in popularity.
const weight = Math.exp((choice.votes + 3), 0.85);
totalWeight += weight;
return { ...choice, weight };
});
//iterate and find amountOfChoices choices
const chosen = [];
while (amountOfChoices-- > 0) {
//weighted random draw of one element of choices
const randomNumber = Math.random() * totalWeight;
let stackWeight = choices[0].weight;
let i = 0;
while (stackWeight < randomNumber) {
stackWeight += choices[++i].weight;
}
//add it to the chosen ones and remove it from the choices before the next iteration
chosen.push(choices[i]);
totalWeight -= choices[i].weight;
choices.splice(i, 1);
}
return chosen;
}
//This function will find segments that are contained inside of eachother, called similar segments
//Only one similar time will be returned, randomly generated based on the sqrt of votes.
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Segments with less than -1 votes are already ignored before this function is called
function chooseSegments(segments) {
//Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically:
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
//2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall
// inside that group (because they're sorted) so we can create a new one
const similarSegmentsGroups = [];
let currentGroup;
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
segments.forEach(segment => {
if (segment.startTime > cursor) {
currentGroup = { segments: [], votes: 0 };
similarSegmentsGroups.push(currentGroup);
}
currentGroup.segments.push(segment);
//only if it is a positive vote, otherwise it is probably just a sponsor time with slightly wrong time
if (segment.votes > 0) {
currentGroup.votes += segment.votes;
}
cursor = Math.max(cursor, segment.endTime);
});
//if there are too many groups, find the best 8
return getWeightedRandomChoice(similarSegmentsGroups, 32).map(
//randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0]
);
}
/**
*
* Returns what would be sent to the client.
* Will respond with errors if required. Returns false if it errors.
*
* @param req
* @param res
*
* @returns
*/
function handleGetSegments(req, res) {
const videoID = req.query.videoID;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
let segments = cleanGetSegments(req, videoID, categories);
if (segments === null || segments === undefined) {
res.sendStatus(500);
return false;
}
if (segments.length == 0) {
res.sendStatus(404);
return false;
}
return segments;
}
module.exports = {
handleGetSegments,
cleanGetSegments,
endpoint: function (req, res) {
let segments = handleGetSegments(req, res);
if (segments) {
//send result
res.send(segments);
}
},
};

View File

@@ -0,0 +1,33 @@
const hashPrefixTester = require('../utils/hashPrefixTester.js');
const getSegments = require('./getSkipSegments.js').cleanGetSegments;
const databases = require('../databases/databases.js');
const logger = require('../utils/logger.js');
const db = databases.db;
module.exports = async function (req, res) {
let hashPrefix = req.params.prefix;
if (!hashPrefixTester(req.params.prefix)) {
res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
return;
}
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
// Get all video id's that match hash prefix
const videoIds = db.prepare('all', 'SELECT DISTINCT videoId, hashedVideoID from sponsorTimes WHERE hashedVideoID LIKE ?', [hashPrefix+'%']);
let segments = videoIds.map((video) => {
return {
videoID: video.videoID,
hash: video.hashedVideoID,
segments: getSegments(req, video.videoID, categories)
};
});
res.status((segments.length === 0) ? 404 : 200).json(segments);
}

92
src/routes/getTopUsers.js Normal file
View File

@@ -0,0 +1,92 @@
var db = require('../databases/databases.js').db;
const logger = require('../utils/logger.js');
const createMemoryCache = require('../utils/createMemoryCache.js');
const config = require('../config.js');
const MILLISECONDS_IN_MINUTE = 60000;
const getTopUsersWithCache = createMemoryCache(generateTopUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE);
function generateTopUsersStats(sortBy, categoryStatsEnabled = false) {
return new Promise((resolve, reject) => {
const userNames = [];
const viewCounts = [];
const totalSubmissions = [];
const minutesSaved = [];
const categoryStats = categoryStatsEnabled ? [] : undefined;
let additionalFields = '';
if (categoryStatsEnabled) {
additionalFields += "SUM(CASE WHEN category = 'sponsor' THEN 1 ELSE 0 END) as categorySponsor, " +
"SUM(CASE WHEN category = 'intro' THEN 1 ELSE 0 END) as categorySumIntro, " +
"SUM(CASE WHEN category = 'outro' THEN 1 ELSE 0 END) as categorySumOutro, " +
"SUM(CASE WHEN category = 'interaction' THEN 1 ELSE 0 END) as categorySumInteraction, " +
"SUM(CASE WHEN category = 'selfpromo' THEN 1 ELSE 0 END) as categorySelfpromo, " +
"SUM(CASE WHEN category = 'music_offtopic' THEN 1 ELSE 0 END) as categoryMusicOfftopic, ";
}
const rows = db.prepare('all', "SELECT COUNT(*) as totalSubmissions, SUM(views) as viewCount," +
"SUM((sponsorTimes.endTime - sponsorTimes.startTime) / 60 * sponsorTimes.views) as minutesSaved, " +
"SUM(votes) as userVotes, " +
additionalFields +
"IFNULL(userNames.userName, sponsorTimes.userID) as userName FROM sponsorTimes LEFT JOIN userNames ON sponsorTimes.userID=userNames.userID " +
"LEFT JOIN privateDB.shadowBannedUsers ON sponsorTimes.userID=privateDB.shadowBannedUsers.userID " +
"WHERE sponsorTimes.votes > -1 AND sponsorTimes.shadowHidden != 1 AND privateDB.shadowBannedUsers.userID IS NULL " +
"GROUP BY IFNULL(userName, sponsorTimes.userID) HAVING userVotes > 20 " +
"ORDER BY " + sortBy + " DESC LIMIT 100", []);
for (let i = 0; i < rows.length; i++) {
userNames[i] = rows[i].userName;
viewCounts[i] = rows[i].viewCount;
totalSubmissions[i] = rows[i].totalSubmissions;
minutesSaved[i] = rows[i].minutesSaved;
if (categoryStatsEnabled) {
categoryStats[i] = [
rows[i].categorySponsor,
rows[i].categorySumIntro,
rows[i].categorySumOutro,
rows[i].categorySumInteraction,
rows[i].categorySelfpromo,
rows[i].categoryMusicOfftopic,
];
}
}
resolve({
userNames,
viewCounts,
totalSubmissions,
minutesSaved,
categoryStats
});
});
}
module.exports = async function getTopUsers (req, res) {
let sortType = req.query.sortType;
let categoryStatsEnabled = req.query.categoryStats;
if (sortType == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//setup which sort type to use
let sortBy = '';
if (sortType == 0) {
sortBy = 'minutesSaved';
} else if (sortType == 1) {
sortBy = 'viewCount';
} else if (sortType == 2) {
sortBy = 'totalSubmissions';
} else {
//invalid request
return res.sendStatus(400);
}
const stats = await getTopUsersWithCache(sortBy, categoryStatsEnabled);
//send this result
res.send(stats);
}

View File

@@ -0,0 +1,69 @@
const db = require('../databases/databases.js').db;
const request = require('request');
const config = require('../config.js');
// A cache of the number of chrome web store users
let chromeUsersCache = null;
let firefoxUsersCache = null;
// By the privacy friendly user counter
let apiUsersCache = null;
let lastUserCountCheck = 0;
module.exports = function getTotalStats (req, res) {
let row = db.prepare('get', "SELECT COUNT(DISTINCT userID) as userCount, COUNT(*) as totalSubmissions, " +
"SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE shadowHidden != 1 AND votes >= 0", []);
if (row !== undefined) {
let extensionUsers = chromeUsersCache + firefoxUsersCache;
//send this result
res.send({
userCount: row.userCount,
activeUsers: extensionUsers,
apiUsers: Math.max(apiUsersCache, extensionUsers),
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved
});
// Check if the cache should be updated (every ~14 hours)
let now = Date.now();
if (now - lastUserCountCheck > 5000000) {
lastUserCountCheck = now;
updateExtensionUsers();
}
}
}
function updateExtensionUsers() {
if (config.userCounterURL) {
request.get(config.userCounterURL + "/api/v1/userCount", (err, response, body) => {
apiUsersCache = Math.max(apiUsersCache, JSON.parse(body).userCount);
});
}
request.get("https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/", function (err, firefoxResponse, body) {
try {
firefoxUsersCache = parseInt(JSON.parse(body).average_daily_users);
request.get("https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone", function(err, chromeResponse, body) {
if (body !== undefined) {
try {
chromeUsersCache = parseInt(body.match(/(?<=\<span class=\"e-f-ih\" title=\").*?(?= users\">)/)[0].replace(",", ""));
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
} else {
lastUserCountCheck = 0;
}
});
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
});
}

82
src/routes/getUserInfo.js Normal file
View File

@@ -0,0 +1,82 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
function dbGetSubmittedSegmentSummary (userID) {
try {
let row = db.prepare("get", "SELECT SUM(((endTime - startTime) / 60) * views) as minutesSaved, count(*) as segmentCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
if (row.minutesSaved != null) {
return {
minutesSaved: row.minutesSaved,
segmentCount: row.segmentCount,
};
} else {
return {
minutesSaved: 0,
segmentCount: 0,
};
}
} catch (err) {
return false;
}
}
function dbGetUsername (userID) {
try {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
if (row !== undefined) {
return row.userName;
} else {
//no username yet, just send back the userID
return userID;
}
} catch (err) {
return false;
}
}
function dbGetViewsForUser (userID) {
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
//increase the view count by one
if (row.viewCount != null) {
return row.viewCount;
} else {
return 0;
}
} catch (err) {
return false;
}
}
function dbGetWarningsForUser (userID) {
try {
let rows = db.prepare('all', "SELECT * FROM warnings WHERE userID = ?", [userID]);
return rows.length;
} catch (err) {
logger.error('Couldn\'t get warnings for user ' + userID + '. returning 0') ;
return 0;
}
}
module.exports = function getUserInfo (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.status(400).send('Parameters are not valid');
return;
}
//hash the userID
userID = getHash(userID);
const segmentsSummary = dbGetSubmittedSegmentSummary(userID);
res.send({
userID,
userName: dbGetUsername(userID),
minutesSaved: segmentsSummary.minutesSaved,
segmentCount: segmentsSummary.segmentCount,
viewCount: dbGetViewsForUser(userID),
warnings: dbGetWarningsForUser(userID)
});
}

37
src/routes/getUsername.js Normal file
View File

@@ -0,0 +1,37 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = function getUsername (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
if (row !== undefined) {
res.send({
userName: row.userName
});
} else {
//no username yet, just send back the userID
res.send({
userName: userID
});
}
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,33 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
var logger = require('../utils/logger.js');
module.exports = function getViewsForUser(req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ?", [userID]);
//increase the view count by one
if (row.viewCount != null) {
res.send({
viewCount: row.viewCount
});
} else {
res.sendStatus(404);
}
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,26 @@
var getSkipSegments = require("./getSkipSegments.js")
module.exports = function (req, res) {
let videoID = req.query.videoID;
let segments = getSkipSegments.handleGetSegments(req, res);
if (segments) {
// Convert to old outputs
let sponsorTimes = [];
let UUIDs = [];
for (const segment of segments) {
sponsorTimes.push(segment.segment);
UUIDs.push(segment.UUID);
}
res.send({
sponsorTimes,
UUIDs
})
}
// Error has already been handled in the other method
}

View File

@@ -0,0 +1,7 @@
var postSkipSegments = require('./postSkipSegments.js');
module.exports = async function submitSponsorTimes(req, res) {
req.query.category = "sponsor";
return postSkipSegments(req, res);
}

View File

@@ -0,0 +1,74 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
module.exports = (req, res) => {
// Collect user input data
let videoID = req.body.videoID;
let userID = req.body.userID;
let categories = req.body.categories;
// Check input data is valid
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
) {
res.status(400).json({
message: 'Bad Format'
});
return;
}
// Check if user is VIP
userID = getHash(userID);
let userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to mark videos.'
});
return;
}
// Get existing no segment markers
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]);
if (!noSegmentList || noSegmentList.length === 0) {
noSegmentList = [];
} else {
noSegmentList = noSegmentList.map((obj) => {
return obj.category;
});
}
// get user categories not already submitted that match accepted format
let categoriesToMark = categories.filter((category) => {
return !!category.match(/^[_a-zA-Z]+$/);
}).filter((category) => {
return noSegmentList.indexOf(category) === -1;
});
// remove any duplicates
categoriesToMark = categoriesToMark.filter((category, index) => {
return categoriesToMark.indexOf(category) === index;
});
// create database entry
categoriesToMark.forEach((category) => {
try {
db.prepare('run', "INSERT INTO noSegments (videoID, userID, category) VALUES(?, ?, ?)", [videoID, userID, category]);
} catch (err) {
logger.error("Error submitting 'noSegment' marker for category '" + category + "' for video '" + videoID + "'");
logger.error(err);
res.status(500).json({
message: "Internal Server Error: Could not write marker to the database."
});
}
});
res.status(200).json({
submitted: categoriesToMark
});
};

View File

@@ -0,0 +1,101 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
const ACTION_NONE = Symbol('none');
const ACTION_UPDATE = Symbol('update');
const ACTION_REMOVE = Symbol('remove');
function shiftSegment(segment, shift) {
if (segment.startTime >= segment.endTime) return {action: ACTION_NONE, segment};
if (shift.startTime >= shift.endTime) return {action: ACTION_NONE, segment};
const duration = shift.endTime - shift.startTime;
if (shift.endTime < segment.startTime) {
// Scenario #1 cut before segment
segment.startTime -= duration;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (shift.startTime > segment.endTime) {
// Scenario #2 cut after segment
return {action: ACTION_NONE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime > shift.endTime) {
// Scenario #3 cut inside segment
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime > shift.endTime) {
// Scenario #4 cut overlap startTime
segment.startTime = shift.startTime;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #5 cut overlap endTime
segment.endTime = shift.startTime;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #6 cut overlap startTime and endTime
return {action: ACTION_REMOVE, segment};
}
return {action: ACTION_NONE, segment};
}
module.exports = (req, res) => {
// Collect user input data
const videoID = req.body.videoID;
const startTime = req.body.startTime;
const endTime = req.body.endTime;
let userID = req.body.userID;
// Check input data is valid
if (!videoID
|| !userID
|| !startTime
|| !endTime
) {
res.status(400).json({
message: 'Bad Format'
});
return;
}
// Check if user is VIP
userID = getHash(userID);
const userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to perform this action.'
});
return;
}
try {
const segments = db.prepare('all', 'SELECT startTime, endTime, UUID FROM sponsorTimes WHERE videoID = ?', [videoID]);
const shift = {
startTime,
endTime,
};
segments.forEach(segment => {
const result = shiftSegment(segment, shift);
switch (result.action) {
case ACTION_UPDATE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ? WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
case ACTION_REMOVE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ?, votes = -2 WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
}
});
}
catch(err) {
logger.error(err);
res.sendStatus(500);
}
res.sendStatus(200);
};

View File

@@ -0,0 +1,530 @@
const config = require('../config.js');
const databases = require('../databases/databases.js');
const db = databases.db;
const privateDB = databases.privateDB;
const YouTubeAPI = require('../utils/youtubeAPI.js');
const logger = require('../utils/logger.js');
const getSubmissionUUID = require('../utils/getSubmissionUUID.js');
const request = require('request');
const isoDurations = require('iso8601-duration');
const fetch = require('node-fetch');
const getHash = require('../utils/getHash.js');
const getIP = require('../utils/getIP.js');
const getFormattedTime = require('../utils/getFormattedTime.js');
const isUserTrustworthy = require('../utils/isUserTrustworthy.js')
const { dispatchEvent } = require('../utils/webhookUtils.js');
function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtubeData, {submissionStart, submissionEnd}, segmentInfo) {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
let userName = row !== undefined ? row.userName : null;
let video = youtubeData.items[0];
let scopeName = "submissions.other";
if (submissionCount <= 1) {
scopeName = "submissions.new";
}
dispatchEvent(scopeName, {
"video": {
"id": videoID,
"title": video.snippet.title,
"thumbnail": video.snippet.thumbnails.maxres ? video.snippet.thumbnails.maxres : null,
"url": "https://www.youtube.com/watch?v=" + videoID
},
"submission": {
"UUID": UUID,
"category": segmentInfo.category,
"startTime": submissionStart,
"endTime": submissionEnd,
"user": {
"UUID": userID,
"username": userName
}
}
});
}
function sendWebhooks(userID, videoID, UUID, segmentInfo) {
if (config.youtubeAPIKey !== null) {
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [userID]);
YouTubeAPI.listVideos(videoID, (err, data) => {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
}
let startTime = parseFloat(segmentInfo.segment[0]);
let endTime = parseFloat(segmentInfo.segment[1]);
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {submissionStart: startTime, submissionEnd: endTime}, segmentInfo);
// If it is a first time submission
// Then send a notification to discord
if (config.discordFirstTimeSubmissionsWebhookURL === null || userSubmissionCountRow.submissionCount > 1) return;
request.post(config.discordFirstTimeSubmissionsWebhookURL, {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"description": "Submission ID: " + UUID +
"\n\nTimestamp: " +
getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n\nCategory: " + segmentInfo.category,
"color": 10813440,
"author": {
"name": userID
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send first time submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending first time submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
});
}
}
function sendWebhooksNB(userID, videoID, UUID, startTime, endTime, category, probability, ytData) {
let submissionInfoRow = db.prepare('get', "SELECT " +
"(select count(1) from sponsorTimes where userID = ?) count, " +
"(select count(1) from sponsorTimes where userID = ? and votes <= -2) disregarded, " +
"coalesce((select userName FROM userNames WHERE userID = ?), ?) userName",
[userID, userID, userID, userID]);
let submittedBy = "";
// If a userName was created then show both
if (submissionInfoRow.userName !== userID){
submittedBy = submissionInfoRow.userName + "\n " + userID;
} else {
submittedBy = userID;
}
// Send discord message
if (config.discordNeuralBlockRejectWebhookURL === null) return;
request.post(config.discordNeuralBlockRejectWebhookURL, {
json: {
"embeds": [{
"title": ytData.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"description": "**Submission ID:** " + UUID +
"\n**Timestamp:** " + getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n**Predicted Probability:** " + probability +
"\n**Category:** " + category +
"\n**Submitted by:** "+ submittedBy +
"\n**Total User Submissions:** "+submissionInfoRow.count +
"\n**Ignored User Submissions:** "+submissionInfoRow.disregarded,
"color": 10813440,
"thumbnail": {
"url": ytData.items[0].snippet.thumbnails.maxres ? ytData.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send NeuralBlock Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending NeuralBlock Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
}
// callback: function(reject: "String containing reason the submission was rejected")
// returns: string when an error, false otherwise
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(submission) {
// Get the video information from the youtube API
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.listVideos(submission.videoID, (err, data) => resolve({err, data}));
});
if (err) {
return false;
} else {
// Check to see if video exists
if (data.pageInfo.totalResults === 0) {
return "No video exists with id " + submission.videoID;
} else {
let segments = submission.segments;
let nbString = "";
for (let i = 0; i < segments.length; i++) {
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
if (duration == 0) {
// Allow submission if the duration is 0 (bug in youtube api)
return false;
} else if ((endTime - startTime) > (duration/100)*80) {
// Reject submission if over 80% of the video
return "One of your submitted segments is over 80% of the video.";
} else {
if (segments[i].category === "sponsor") {
//Prepare timestamps to send to NB all at once
nbString = nbString + segments[i].segment[0] + "," + segments[i].segment[1] + ";";
}
}
}
// Check NeuralBlock
let neuralBlockURL = config.neuralBlockURL;
if (!neuralBlockURL) return false;
let response = await fetch(neuralBlockURL + "/api/checkSponsorSegments?vid=" + submission.videoID +
"&segments=" + nbString.substring(0,nbString.length-1));
if (!response.ok) return false;
let nbPredictions = await response.json();
nbDecision = false;
let predictionIdx = 0; //Keep track because only sponsor categories were submitted
for (let i = 0; i < segments.length; i++){
if (segments[i].category === "sponsor"){
if (nbPredictions.probabilities[predictionIdx] < 0.70){
nbDecision = true; // At least one bad entry
startTime = parseFloat(segments[i].segment[0]);
endTime = parseFloat(segments[i].segment[1]);
const UUID = getSubmissionUUID(submission.videoID, segments[i].category, submission.userID, startTime, endTime);
// Send to Discord
// Note, if this is too spammy. Consider sending all the segments as one Webhook
sendWebhooksNB(submission.userID, submission.videoID, UUID, startTime, endTime, segments[i].category, nbPredictions.probabilities[predictionIdx], data);
}
predictionIdx++;
}
}
if (nbDecision){
return "Rejected based on NeuralBlock predictions.";
} else {
return false;
}
}
}
} else {
logger.debug("Skipped YouTube API");
// Can't moderate the submission without calling the youtube API
// so allow by default.
return false;
}
}
function proxySubmission(req) {
request.post(config.proxySubmission + '/api/skipSegments?userID='+req.query.userID+'&videoID='+req.query.videoID, {json: req.body}, (err, result) => {
if (config.mode === 'development') {
if (!err) {
logger.debug('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
} else {
logger.error("Proxy Submission: Failed to make call");
}
}
});
}
module.exports = async function postSkipSegments(req, res) {
if (config.proxySubmission) {
proxySubmission(req);
}
let videoID = req.query.videoID || req.body.videoID;
let userID = req.query.userID || req.body.userID;
let segments = req.body.segments;
if (segments === undefined) {
// Use query instead
segments = [{
segment: [req.query.startTime, req.query.endTime],
category: req.query.category
}];
}
const invalidFields = [];
if (typeof videoID !== 'string') {
invalidFields.push('videoID');
}
if (typeof userID !== 'string') {
invalidFields.push('userID');
}
if (!Array.isArray(segments) || segments.length < 1) {
invalidFields.push('segments');
}
if (invalidFields.length !== 0) {
// invalid request
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ', ' : '') + c, '');
res.status(400).send(`No valid ${fields} field(s) provided`);
return;
}
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
let warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[userID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))]
).count;
if (warningsCount >= config.maxNumberOfActiveWarnings) {
return res.status(403).send('Submission blocked. Too many active warnings!');
}
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]).map((list) => { return list.category });
//check if this user is on the vip list
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
let decreaseVotes = 0;
// Check if all submissions are correct
for (let i = 0; i < segments.length; i++) {
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
//invalid request
res.status(400).send("One of your segments are invalid");
return;
}
if (!config.categoryList.includes(segments[i].category)) {
res.status("400").send("Category doesn't exist.");
return;
}
// Reject segemnt if it's in the no segments list
if (!isVIP && noSegmentList.indexOf(segments[i].category) !== -1) {
// TODO: Do something about the fradulent submission
logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
res.status(403).send(
"Request rejected by auto moderator: New submissions are not allowed for the following category: '"
+ segments[i].category + "'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n "
+ (segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. " +
"Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n " : "")
+ "If you believe this is incorrect, please contact someone on Discord."
);
return;
}
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
if (isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) {
//invalid request
res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
return;
}
if (segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
// Too short
res.status(400).send("Sponsors must be longer than 1 second long");
return;
}
//check if this info has already been submitted before
let duplicateCheck2Row = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE startTime = ? " +
"and endTime = ? and category = ? and videoID = ?", [startTime, endTime, segments[i].category, videoID]);
if (duplicateCheck2Row.count > 0) {
res.sendStatus(409);
return;
}
}
// Auto moderator check
if (!isVIP) {
let autoModerateResult = await autoModerateSubmission({userID, videoID, segments});//startTime, endTime, category: segments[i].category});
if (autoModerateResult == "Rejected based on NeuralBlock predictions."){
// If NB automod rejects, the submission will start with -2 votes.
// Note, if one submission is bad all submissions will be affected.
// However, this behavior is consistent with other automod functions
// already in place.
//decreaseVotes = -2; //Disable for now
} else if (autoModerateResult) {
//Normal automod behavior
res.status(403).send("Request rejected by auto moderator: " + autoModerateResult + " If this is an issue, send a message on Discord.");
return;
}
}
// Will be filled when submitting
let UUIDs = [];
try {
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
// Disable IP ratelimiting for now
if (false) {
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?", [hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
return;
}
}
// Disable max submissions for now
if (false) {
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?", [userID, videoID]);
if (duplicateCheckRow.count >= 16) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
return;
}
}
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
let shadowBanned = shadowBanRow.userCount;
if (!(await isUserTrustworthy(userID))) {
//hide this submission as this user is untrustworthy
shadowBanned = 1;
}
let startingVotes = 0 + decreaseVotes;
if (isVIP) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10000;
}
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.listVideos(videoID, (err, data) => resolve({err, data}));
});
if (err) {
logger.error("Error while submitting when connecting to YouTube API: " + err);
} else {
//get all segments for this video and user
let allSubmittedByUser = db.prepare('all', "SELECT startTime, endTime FROM sponsorTimes WHERE userID = ? and videoID = ? and votes > -1", [userID, videoID]);
let allSegmentTimes = [];
if (allSubmittedByUser !== undefined) {
//add segments the user has previously submitted
for (const segmentInfo of allSubmittedByUser) {
allSegmentTimes.push([parseFloat(segmentInfo.startTime), parseFloat(segmentInfo.endTime)])
}
}
//add segments they are trying to add in this submission
for (let i = 0; i < segments.length; i++) {
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
allSegmentTimes.push([startTime, endTime]);
}
//merge all the times into non-overlapping arrays
const allSegmentsSorted = mergeTimeSegments(allSegmentTimes.sort(function(a, b) { return a[0]-b[0] || a[1]-b[1] }));
let videoDuration = data.items[0].contentDetails.duration;
videoDuration = isoDurations.toSeconds(isoDurations.parse(videoDuration));
if (videoDuration != 0) {
let allSegmentDuration = 0;
//sum all segment times together
allSegmentsSorted.forEach(segmentInfo => allSegmentDuration += segmentInfo[1] - segmentInfo[0]);
if (allSegmentDuration > (videoDuration/100)*80) {
// Reject submission if all segments combine are over 80% of the video
res.status(400).send("Total length of your submitted segments are over 80% of the video.");
return;
}
}
}
}
for (const segmentInfo of segments) {
//this can just be a hash of the data
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
const UUID = getSubmissionUUID(videoID, segmentInfo.category, userID, segmentInfo.segment[0], segmentInfo.segment[1]);
try {
db.prepare('run', "INSERT INTO sponsorTimes " +
"(videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID)" +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned, getHash(videoID, 1)
]
);
//add to private db as well
privateDB.prepare('run', "INSERT INTO sponsorTimes VALUES(?, ?, ?)", [videoID, hashedIP, timeSubmitted]);
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category + ". " + err);
return;
}
UUIDs.push(UUID);
}
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
res.sendStatus(200);
for (let i = 0; i < segments.length; i++) {
sendWebhooks(userID, videoID, UUIDs[i], segments[i]);
}
}
// Takes an array of arrays:
// ex)
// [
// [3, 40],
// [50, 70],
// [60, 80],
// [100, 150]
// ]
// => transforms to combining overlapping segments
// [
// [3, 40],
// [50, 80],
// [100, 150]
// ]
function mergeTimeSegments(ranges) {
var result = [], last;
ranges.forEach(function (r) {
if (!last || r[0] > last[1])
result.push(last = r);
else if (r[1] > last[1])
last[1] = r[1];
});
return result;
}

24
src/routes/postWarning.js Normal file
View File

@@ -0,0 +1,24 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
module.exports = (req, res) => {
// Collect user input data
let issuerUserID = getHash(req.body.issuerUserID);
let userID = getHash(req.body.userID);
let issueTime = new Date().getTime();
// Ensure user is a VIP
if (!isUserVIP(issuerUserID)) {
logger.debug("Permission violation: User " + issuerUserID + " attempted to warn user " + userID + "."); // maybe warn?
res.status(403).json({"message": "Not a VIP"});
return;
}
db.prepare('run', 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES (?, ?, ?)', [userID, issueTime, issuerUserID]);
res.status(200).json({
message: "Warning issued to user '" + userID + "'."
});
};

60
src/routes/setUsername.js Normal file
View File

@@ -0,0 +1,60 @@
var config = require('../config.js');
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = function setUsername(req, res) {
let userID = req.query.userID;
let userName = req.query.username;
let adminUserIDInput = req.query.adminUserID;
if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 64) {
//invalid request
res.sendStatus(400);
return;
}
if (userName.includes("discord")) {
// Don't allow
res.sendStatus(200);
return;
}
if (adminUserIDInput != undefined) {
//this is the admin controlling the other users account, don't hash the controling account's ID
adminUserIDInput = getHash(adminUserIDInput);
if (adminUserIDInput != config.adminUserID) {
//they aren't the admin
res.sendStatus(403);
return;
}
} else {
//hash the userID
userID = getHash(userID);
}
try {
//check if username is already set
let row = db.prepare('get', "SELECT count(*) as count FROM userNames WHERE userID = ?", [userID]);
if (row.count > 0) {
//already exists, update this row
db.prepare('run', "UPDATE userNames SET userName = ? WHERE userID = ?", [userName, userID]);
} else {
//add to the db
db.prepare('run', "INSERT INTO userNames VALUES(?, ?)", [userID, userName]);
}
res.sendStatus(200);
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

100
src/routes/shadowBanUser.js Normal file
View File

@@ -0,0 +1,100 @@
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = async function shadowBanUser(req, res) {
let userID = req.query.userID;
let hashedIP = req.query.hashedIP;
let adminUserIDInput = req.query.adminUserID;
let enabled = req.query.enabled;
if (enabled === undefined){
enabled = true;
} else {
enabled = enabled === "true";
}
//if enabled is false and the old submissions should be made visible again
let unHideOldSubmissions = req.query.unHideOldSubmissions !== "false";
if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
adminUserIDInput = getHash(adminUserIDInput);
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [adminUserIDInput]).userCount > 0;
if (!isVIP) {
//not authorized
res.sendStatus(403);
return;
}
if (userID) {
//check to see if this user is already shadowbanned
let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the shadow ban list
//add it to the table
privateDB.prepare('run', "INSERT INTO shadowBannedUsers VALUES(?)", [userID]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE userID = ?", [userID]);
}
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
//find all previous submissions and unhide them
if (unHideOldSubmissions) {
let segmentsToIgnore = db.prepare('all', "SELECT uuid FROM sponsorTimes st JOIN noSegments ns on st.videoID = ns.videoID AND st.category = ns.category WHERE st.userID = ?", [userID]).map((item) => item.UUID);
let allSegments = db.prepare('all', "SELECT uuid FROM sponsorTimes st WHERE st.userID = ?", [userID]).map((item) => item.UUID);
allSegments.filter((item) => {
return segmentsToIgnore.indexOf(item) === -1;
}).forEach((uuid) => {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE uuid = ?", [uuid]);
});
}
}
} else if (hashedIP) {
//check to see if this user is already shadowbanned
// let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedIPs WHERE hashedIP = ?", [hashedIP]);
// if (enabled && row.userCount == 0) {
if (enabled) {
//add them to the shadow ban list
//add it to the table
// privateDB.prepare('run', "INSERT INTO shadowBannedIPs VALUES(?)", [hashedIP]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE timeSubmitted IN " +
"(SELECT privateDB.timeSubmitted FROM sponsorTimes LEFT JOIN privateDB.sponsorTimes as privateDB ON sponsorTimes.timeSubmitted=privateDB.timeSubmitted " +
"WHERE privateDB.hashedIP = ?)", [hashedIP]);
}
} else if (!enabled && row.userCount > 0) {
// //remove them from the shadow ban list
// privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
// //find all previous submissions and unhide them
// if (unHideOldSubmissions) {
// db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
// }
}
}
res.sendStatus(200);
}

View File

@@ -0,0 +1,16 @@
var db = require('../databases/databases.js').db;
module.exports = function viewedVideoSponsorTime(req, res) {
let UUID = req.query.UUID;
if (UUID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//up the view count by one
db.prepare('run', "UPDATE sponsorTimes SET views = views + 1 WHERE UUID = ?", [UUID]);
res.sendStatus(200);
}

View File

@@ -0,0 +1,403 @@
var config = require('../config.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
var isUserTrustworthy = require('../utils/isUserTrustworthy.js');
const { getVoteAuthor, getVoteAuthorRaw, dispatchEvent } = require('../utils/webhookUtils.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var YouTubeAPI = require('../utils/youtubeAPI.js');
var request = require('request');
const logger = require('../utils/logger.js');
const isUserVIP = require('../utils/isUserVIP.js');
const voteTypes = {
normal: 0,
incorrect: 1
}
/**
* @param {Object} voteData
* @param {string} voteData.UUID
* @param {string} voteData.nonAnonUserID
* @param {number} voteData.voteTypeEnum
* @param {boolean} voteData.isVIP
* @param {boolean} voteData.isOwnSubmission
* @param voteData.row
* @param {string} voteData.category
* @param {number} voteData.incrementAmount
* @param {number} voteData.oldIncrementAmount
*/
function sendWebhooks(voteData) {
let submissionInfoRow = db.prepare('get', "SELECT s.videoID, s.userID, s.startTime, s.endTime, s.category, u.userName, " +
"(select count(1) from sponsorTimes where userID = s.userID) count, " +
"(select count(1) from sponsorTimes where userID = s.userID and votes <= -2) disregarded " +
"FROM sponsorTimes s left join userNames u on s.userID = u.userID where s.UUID=?",
[voteData.UUID]);
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [voteData.nonAnonUserID]);
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
let webhookURL = null;
if (voteData.voteTypeEnum === voteTypes.normal) {
webhookURL = config.discordReportChannelWebhookURL;
} else if (voteData.voteTypeEnum === voteTypes.incorrect) {
webhookURL = config.discordCompletelyIncorrectReportWebhookURL;
}
if (config.youtubeAPIKey !== null) {
YouTubeAPI.listVideos(submissionInfoRow.videoID, (err, data) => {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
}
let isUpvote = voteData.incrementAmount > 0;
// Send custom webhooks
dispatchEvent(isUpvote ? "vote.up" : "vote.down", {
"user": {
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
},
"video": {
"id": submissionInfoRow.videoID,
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID,
"thumbnail": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : ""
},
"submission": {
"UUID": voteData.UUID,
"views": voteData.row.views,
"category": voteData.category,
"startTime": submissionInfoRow.startTime,
"endTime": submissionInfoRow.endTime,
"user": {
"UUID": submissionInfoRow.userID,
"username": submissionInfoRow.userName,
"submissions": {
"total": submissionInfoRow.count,
"ignored": submissionInfoRow.disregarded
}
}
},
"votes": {
"before": voteData.row.votes,
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount)
}
});
// Send discord message
if (webhookURL !== null && !isUpvote) {
request.post(webhookURL, {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
+ "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2),
"description": "**" + voteData.row.votes + " Votes Prior | " +
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
+ " Views**\n\n**Submission ID:** " + voteData.UUID
+ "\n**Category:** " + submissionInfoRow.category
+ "\n\n**Submitted by:** "+submissionInfoRow.userName+"\n " + submissionInfoRow.userID
+ "\n\n**Total User Submissions:** "+submissionInfoRow.count
+ "\n**Ignored User Submissions:** "+submissionInfoRow.disregarded
+"\n\n**Timestamp:** " +
getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime),
"color": 10813440,
"author": {
"name": getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send reported submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending reported submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
}
});
}
}
}
function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
// Check if they've already made a vote
let previousVoteInfo = privateDB.prepare('get', "select count(*) as votes, category from categoryVotes where UUID = ? and userID = ?", [UUID, userID]);
if (previousVoteInfo !== undefined && previousVoteInfo.category === category) {
// Double vote, ignore
res.sendStatus(200);
return;
}
let currentCategory = db.prepare('get', "select category from sponsorTimes where UUID = ?", [UUID]);
if (!currentCategory) {
// Submission doesn't exist
res.status("400").send("Submission doesn't exist.");
return;
}
if (!config.categoryList.includes(category)) {
res.status("400").send("Category doesn't exist.");
return;
}
let timeSubmitted = Date.now();
let voteAmount = isVIP ? 500 : 1;
// Add the vote
if (db.prepare('get', "select count(*) as count from categoryVotes where UUID = ? and category = ?", [UUID, category]).count > 0) {
// Update the already existing db entry
db.prepare('run', "update categoryVotes set votes = votes + ? where UUID = ? and category = ?", [voteAmount, UUID, category]);
} else {
// Add a db entry
db.prepare('run', "insert into categoryVotes (UUID, category, votes) values (?, ?, ?)", [UUID, category, voteAmount]);
}
// Add the info into the private db
if (previousVoteInfo !== undefined) {
// Reverse the previous vote
db.prepare('run', "update categoryVotes set votes = votes - ? where UUID = ? and category = ?", [voteAmount, UUID, previousVoteInfo.category]);
privateDB.prepare('run', "update categoryVotes set category = ?, timeSubmitted = ?, hashedIP = ? where userID = ?", [category, timeSubmitted, hashedIP, userID]);
} else {
privateDB.prepare('run', "insert into categoryVotes (UUID, userID, hashedIP, category, timeSubmitted) values (?, ?, ?, ?, ?)", [UUID, userID, hashedIP, category, timeSubmitted]);
}
// See if the submissions category is ready to change
let currentCategoryInfo = db.prepare("get", "select votes from categoryVotes where UUID = ? and category = ?", [UUID, currentCategory.category]);
let submissionInfo = db.prepare("get", "SELECT userID, timeSubmitted, votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
let isSubmissionVIP = submissionInfo && isUserVIP(submissionInfo.userID);
let startingVotes = isSubmissionVIP ? 10000 : 1;
// Change this value from 1 in the future to make it harder to change categories
// Done this way without ORs incase the value is zero
let currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
// Add submission as vote
if (!currentCategoryInfo && submissionInfo) {
db.prepare("run", "insert into categoryVotes (UUID, category, votes) values (?, ?, ?)", [UUID, currentCategory.category, currentCategoryCount]);
privateDB.prepare("run", "insert into categoryVotes (UUID, userID, hashedIP, category, timeSubmitted) values (?, ?, ?, ?, ?)", [UUID, submissionInfo.userID, "unknown", currentCategory.category, submissionInfo.timeSubmitted]);
}
let nextCategoryCount = (previousVoteInfo.votes || 0) + voteAmount;
//TODO: In the future, raise this number from zero to make it harder to change categories
// VIPs change it every time
if (nextCategoryCount - currentCategoryCount >= (submissionInfo ? Math.max(Math.ceil(submissionInfo.votes / 2), 1) : 1) || isVIP) {
// Replace the category
db.prepare('run', "update sponsorTimes set category = ? where UUID = ?", [category, UUID]);
}
res.sendStatus(200);
}
async function voteOnSponsorTime(req, res) {
let UUID = req.query.UUID;
let userID = req.query.userID;
let type = req.query.type;
let category = req.query.category;
if (UUID === undefined || userID === undefined || (type === undefined && category === undefined)) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
let nonAnonUserID = getHash(userID);
userID = getHash(userID + UUID);
//x-forwarded-for if this server is behind a proxy
let ip = getIP(req);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(ip + config.globalSalt);
//check if this user is on the vip list
let isVIP = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [nonAnonUserID]).userCount > 0;
//check if user voting on own submission
let isOwnSubmission = db.prepare("get", "SELECT UUID as submissionCount FROM sponsorTimes where userID = ? AND UUID = ?", [nonAnonUserID, UUID]) !== undefined;
if (type === undefined && category !== undefined) {
return categoryVote(UUID, nonAnonUserID, isVIP, category, hashedIP, res);
}
if (type == 1 && !isVIP && !isOwnSubmission) {
// Check if upvoting hidden segment
let voteInfo = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (voteInfo && voteInfo.votes <= -2) {
res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.")
return;
}
}
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
let warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))]
).count;
if (warningsCount >= config.maxNumberOfActiveWarnings) {
return res.status(403).send('Vote blocked. Too many active warnings!');
}
let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
try {
//check if vote has already happened
let votesRow = privateDB.prepare('get', "SELECT type FROM votes WHERE userID = ? AND UUID = ?", [userID, UUID]);
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
let oldIncrementAmount = 0;
if (type == 1 || type == 11) {
//upvote
incrementAmount = 1;
} else if (type == 0 || type == 10) {
//downvote
incrementAmount = -1;
} else if (type == 20) {
//undo/cancel vote
incrementAmount = 0;
} else {
//unrecongnised type of vote
res.sendStatus(400);
return;
}
if (votesRow != undefined) {
if (votesRow.type === 1 || type === 11) {
//upvote
oldIncrementAmount = 1;
} else if (votesRow.type === 0 || type === 10) {
//downvote
oldIncrementAmount = -1;
} else if (votesRow.type === 2) {
//extra downvote
oldIncrementAmount = -4;
} else if (votesRow.type === 20) {
//undo/cancel vote
oldIncrementAmount = 0;
} else if (votesRow.type < 0) {
//vip downvote
oldIncrementAmount = votesRow.type;
} else if (votesRow.type === 12) {
// VIP downvote for completely incorrect
oldIncrementAmount = -500;
} else if (votesRow.type === 13) {
// VIP upvote for completely incorrect
oldIncrementAmount = 500;
}
}
//check if the increment amount should be multiplied (downvotes have more power if there have been many views)
let row = db.prepare('get', "SELECT votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (voteTypeEnum === voteTypes.normal) {
if ((isVIP || isOwnSubmission) && incrementAmount < 0) {
//this user is a vip and a downvote
incrementAmount = - (row.votes + 2 - oldIncrementAmount);
type = incrementAmount;
}
} else if (voteTypeEnum == voteTypes.incorrect) {
if (isVIP || isOwnSubmission) {
//this user is a vip and a downvote
incrementAmount = 500 * incrementAmount;
type = incrementAmount < 0 ? 12 : 13;
}
}
// Only change the database if they have made a submission before and haven't voted recently
let ableToVote = isVIP
|| (db.prepare("get", "SELECT userID FROM sponsorTimes WHERE userID = ?", [nonAnonUserID]) !== undefined
&& privateDB.prepare("get", "SELECT userID FROM shadowBannedUsers WHERE userID = ?", [nonAnonUserID]) === undefined
&& privateDB.prepare("get", "SELECT UUID FROM votes WHERE UUID = ? AND hashedIP = ? AND userID != ?", [UUID, hashedIP, userID]) === undefined);
if (ableToVote) {
//update the votes table
if (votesRow != undefined) {
privateDB.prepare('run', "UPDATE votes SET type = ? WHERE userID = ? AND UUID = ?", [type, userID, UUID]);
} else {
privateDB.prepare('run', "INSERT INTO votes VALUES(?, ?, ?, ?)", [UUID, userID, hashedIP, type]);
}
let columnName = "";
if (voteTypeEnum === voteTypes.normal) {
columnName = "votes";
} else if (voteTypeEnum === voteTypes.incorrect) {
columnName = "incorrectVotes";
}
//update the vote count on this sponsorTime
//oldIncrementAmount will be zero is row is null
db.prepare('run', "UPDATE sponsorTimes SET " + columnName + " = " + columnName + " + ? WHERE UUID = ?", [incrementAmount - oldIncrementAmount, UUID]);
//for each positive vote, see if a hidden submission can be shown again
if (incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
//find the UUID that submitted the submission that was voted on
let submissionUserIDInfo = db.prepare('get', "SELECT userID FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (!submissionUserIDInfo) {
// They are voting on a non-existent submission
res.status(400).send("Voting on a non-existent submission");
return;
}
let submissionUserID = submissionUserIDInfo.userID;
//check if any submissions are hidden
let hiddenSubmissionsRow = db.prepare('get', "SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0", [submissionUserID]);
if (hiddenSubmissionsRow.hiddenSubmissions > 0) {
//see if some of this users submissions should be visible again
if (await isUserTrustworthy(submissionUserID)) {
//they are trustworthy again, show 2 of their submissions again, if there are two to show
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE ROWID IN (SELECT ROWID FROM sponsorTimes WHERE userID = ? AND shadowHidden = 1 LIMIT 2)", [submissionUserID]);
}
}
}
}
res.sendStatus(200);
if (incrementAmount - oldIncrementAmount !== 0) {
sendWebhooks({
UUID,
nonAnonUserID,
voteTypeEnum,
isVIP,
isOwnSubmission,
row,
category,
incrementAmount,
oldIncrementAmount
});
}
} catch (err) {
logger.error(err);
res.status(500).json({error: 'Internal error creating segment vote'});
}
}
module.exports = {
voteOnSponsorTime,
endpoint: voteOnSponsorTime
};

View File

@@ -0,0 +1,44 @@
module.exports = function createMemoryCache(memoryFn, cacheTimeMs) {
if (isNaN(cacheTimeMs)) cacheTimeMs = 0;
// holds the promise results
const cache = new Map();
// holds the promises that are not fulfilled
const promiseMemory = new Map();
return (...args) => {
// create cacheKey by joining arguments as string
const cacheKey = args.join('.');
// check if promising is already running
if (promiseMemory.has(cacheKey)) {
return promiseMemory.get(cacheKey);
}
else {
// check if result is in cache
if (cache.has(cacheKey)) {
const cacheItem = cache.get(cacheKey);
const now = Date.now();
// check if cache is valid
if (!(cacheItem.cacheTime + cacheTimeMs < now)) {
return Promise.resolve(cacheItem.result);
}
}
// create new promise
const promise = new Promise(async (resolve, reject) => {
resolve((await memoryFn(...args)));
});
// store promise reference until fulfilled
promiseMemory.set(cacheKey, promise);
return promise.then(result => {
// store promise result in cache
cache.set(cacheKey, {
result,
cacheTime: Date.now(),
});
// remove fulfilled promise from memory
promiseMemory.delete(cacheKey);
// return promise result
return result;
});
}
};
};

View File

@@ -0,0 +1,14 @@
//converts time in seconds to minutes:seconds
module.exports = function getFormattedTime(totalSeconds) {
let minutes = Math.floor(totalSeconds / 60);
let seconds = totalSeconds - minutes * 60;
let secondsDisplay = seconds.toFixed(3);
if (seconds < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}
let formatted = minutes+ ":" + secondsDisplay;
return formatted;
}

12
src/utils/getHash.js Normal file
View File

@@ -0,0 +1,12 @@
var crypto = require('crypto');
module.exports = function (value, times=5000) {
if (times <= 0) return "";
for (let i = 0; i < times; i++) {
let hashCreator = crypto.createHash('sha256');
value = hashCreator.update(value).digest('hex');
}
return value;
}

16
src/utils/getIP.js Normal file
View File

@@ -0,0 +1,16 @@
var config = require('../config.js');
module.exports = function getIP(req) {
if (config.behindProxy === true || config.behindProxy === "true") config.behindProxy = "X-Forwarded-For";
switch (config.behindProxy) {
case "X-Forwarded-For":
return req.headers['x-forwarded-for'];
case "Cloudflare":
return req.headers['cf-connecting-ip'];
case "X-Real-IP":
return req.headers['x-real-ip'];
default:
return req.connection.remoteAddress;
}
}

View File

@@ -0,0 +1,7 @@
const getHash = require('./getHash.js');
module.exports = function getSubmissionUUID(videoID, category, userID,
startTime, endTime) {
return getHash('v2-categories' + videoID + startTime + endTime + category +
userID, 1);
};

View File

@@ -0,0 +1,10 @@
const config = require('../config.js');
const minimumPrefix = config.minimumPrefix || '3';
const maximumPrefix = config.maximumPrefix || '32'; // Half the hash.
const prefixChecker = new RegExp('^[\\da-f]{' + minimumPrefix + ',' + maximumPrefix + '}$', 'i');
module.exports = (prefix) => {
return prefixChecker.test(prefix);
};

View File

@@ -0,0 +1,20 @@
var databases = require('../databases/databases.js');
var db = databases.db;
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
module.exports = async (userID) => {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare('get', "SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?", [userID]);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare('get', "SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)", [userID]);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}

8
src/utils/isUserVIP.js Normal file
View File

@@ -0,0 +1,8 @@
const databases = require('../databases/databases.js');
const db = databases.db;
module.exports = (userID) => {
return db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
}

70
src/utils/logger.js Normal file
View File

@@ -0,0 +1,70 @@
const config = require('../config.js');
const levels = {
ERROR: "ERROR",
WARN: "WARN",
INFO: "INFO",
DEBUG: "DEBUG"
};
const colors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m",
}
const settings = {
ERROR: true,
WARN: true,
INFO: false,
DEBUG: false
};
if (config.mode === 'development') {
settings.INFO = true;
settings.DEBUG = true;
} else if (config.mode === 'test') {
settings.WARN = false;
}
function log(level, string) {
if (!!settings[level]) {
let color = colors.Bright;
if (level === levels.ERROR) color = colors.FgRed;
if (level === levels.WARN) color = colors.FgYellow;
if (level.length === 4) {level = level + " "}; // ensure logs are aligned
console.log(colors.Dim, level + " " + new Date().toISOString() + ": ", color, string, colors.Reset);
}
}
module.exports = {
levels,
log,
error: (string) => {log(levels.ERROR, string)},
warn: (string) => {log(levels.WARN, string)},
info: (string) => {log(levels.INFO, string)},
debug: (string) => {log(levels.DEBUG, string)},
};

18
src/utils/redis.js Normal file
View File

@@ -0,0 +1,18 @@
const config = require('../config.js');
const logger = require('./logger.js');
if (config.redis) {
const redis = require('redis');
logger.info('Connected to redis');
const client = redis.createClient(config.redis);
module.exports = client;
} else {
module.exports = {
get: (key, callback) => {
callback(false);
},
set: (key, value, callback) => {
callback(false);
}
};
}

52
src/utils/webhookUtils.js Normal file
View File

@@ -0,0 +1,52 @@
const config = require('../config.js');
const logger = require('../utils/logger.js');
const request = require('request');
function getVoteAuthorRaw(submissionCount, isVIP, isOwnSubmission) {
if (isOwnSubmission) {
return "self";
} else if (isVIP) {
return "vip";
} else if (submissionCount === 0) {
return "new";
} else {
return "other";
};
};
function getVoteAuthor(submissionCount, isVIP, isOwnSubmission) {
if (submissionCount === 0) {
return "Report by New User";
} else if (isOwnSubmission) {
return "Report by Submitter";
} else if (isVIP) {
return "Report by VIP User";
}
return "";
}
function dispatchEvent(scope, data) {
let webhooks = config.webhooks;
if (webhooks === undefined || webhooks.length === 0) return;
logger.debug("Dispatching webhooks");
webhooks.forEach(webhook => {
let webhookURL = webhook.url;
let authKey = webhook.key;
let scopes = webhook.scopes || [];
if (!scopes.includes(scope.toLowerCase())) return;
request.post(webhookURL, {json: data, headers: {
"Authorization": authKey,
"Event-Type": scope // Maybe change this in the future?
}}).on('error', (e) => {
logger.warn('Couldn\'t send webhook to ' + webhook.url);
logger.warn(e);
});
});
}
module.exports = {
getVoteAuthorRaw,
getVoteAuthor,
dispatchEvent
}

62
src/utils/youtubeAPI.js Normal file
View File

@@ -0,0 +1,62 @@
var config = require('../config.js');
// YouTube API
const YouTubeAPI = require("youtube-api");
const redis = require('./redis.js');
const logger = require('./logger.js');
var exportObject;
// If in test mode, return a mocked youtube object
// otherwise return an authenticated youtube api
if (config.mode === "test") {
exportObject = require("../../test/youtubeMock.js");
} else {
YouTubeAPI.authenticate({
type: "key",
key: config.youtubeAPIKey
});
exportObject = YouTubeAPI;
// YouTubeAPI.videos.list wrapper with cacheing
exportObject.listVideos = (videoID, callback) => {
let part = 'contentDetails,snippet';
if (videoID.length !== 11 || videoID.includes(".")) {
callback("Invalid video ID");
return;
}
let redisKey = "youtube.video." + videoID;
redis.get(redisKey, (getErr, result) => {
if (getErr || !result) {
logger.debug("redis: no cache for video information: " + videoID);
YouTubeAPI.videos.list({
part,
id: videoID
}, (ytErr, data) => {
if (!ytErr) {
// Only set cache if data returned
if (data.items.length > 0) {
redis.set(redisKey, JSON.stringify(data), (setErr) => {
if(setErr) {
logger.warn(setErr);
} else {
logger.debug("redis: video information cache set for: " + videoID);
}
callback(false, data); // don't fail
});
} else {
callback(false, data); // don't fail
}
} else {
callback(ytErr, data)
}
});
} else {
logger.debug("redis: fetched video information from cache: " + videoID);
callback(getErr, JSON.parse(result));
}
});
};
}
module.exports = exportObject;

41
test.js Normal file
View File

@@ -0,0 +1,41 @@
var Mocha = require('mocha'),
fs = require('fs'),
path = require('path');
var config = require('./src/config.js');
// delete old test database
if (fs.existsSync(config.db)) fs.unlinkSync(config.db);
if (fs.existsSync(config.privateDB)) fs.unlinkSync(config.privateDB);
var createServer = require('./src/app.js');
var createMockServer = require('./test/mocks.js');
const logger = require('./src/utils/logger.js');
// Instantiate a Mocha instance.
var mocha = new Mocha();
var testDir = './test/cases'
// Add each .js file to the mocha instance
fs.readdirSync(testDir).filter(function(file) {
// Only keep the .js files
return file.substr(-3) === '.js';
}).forEach(function(file) {
mocha.addFile(
path.join(testDir, file)
);
});
var mockServer = createMockServer(() => {
logger.info("Started mock HTTP Server");
var server = createServer(() => {
logger.info("Started main HTTP server");
// Run the tests.
mocha.run((failures) => {
mockServer.close();
server.close();
process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures
});
});
});

68
test.json Normal file
View File

@@ -0,0 +1,68 @@
{
"port": 8080,
"mockPort": 8081,
"globalSalt": "testSalt",
"adminUserID": "testUserId",
"youtubeAPIKey": "",
"discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook",
"discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook",
"discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook",
"discordNeuralBlockRejectWebhookURL": "http://127.0.0.1:8081/NeuralBlockRejectWebhook",
"neuralBlockURL": "http://127.0.0.1:8081/NeuralBlock",
"behindProxy": true,
"db": "./test/databases/sponsorTimes.db",
"privateDB": "./test/databases/private.db",
"createDatabaseIfNotExist": true,
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "test",
"readOnly": false,
"webhooks": [
{
"url": "http://127.0.0.1:8081/CustomWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://127.0.0.1:8081/FailedWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://127.0.0.1:8099/WrongPort",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://unresolvable.host:8081/FailedWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}
],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"rateLimit": {
"vote": {
"windowMs": 900000,
"max": 20,
"message": "Too many votes, please try again later",
"statusCode": 429
},
"view": {
"windowMs": 900000,
"max": 20,
"statusCode": 200
}
}
}

12
test/cases/dbUpgrade.js Normal file
View File

@@ -0,0 +1,12 @@
const databases = require('../../src/databases/databases.js');
const db = databases.db;
const privateDB = databases.privateDB;
describe('dbUpgrade', () => {
it('Should update the database version when starting the application', (done) => {
let dbVersion = db.prepare('get', 'SELECT key, value FROM config where key = ?', ['version']).value;
let privateVersion = privateDB.prepare('get', 'SELECT key, value FROM config where key = ?', ['version']).value;
if (dbVersion >= 1 && privateVersion >= 1) done();
else done('Versions are not at least 1. db is ' + dbVersion + ', private is ' + privateVersion);
});
});

33
test/cases/getHash.js Normal file
View File

@@ -0,0 +1,33 @@
var getHash = require('../../src/utils/getHash.js');
var assert = require('assert');
describe('getHash', () => {
it('Should not output the input string', () => {
assert(getHash("test") !== "test");
assert(getHash("test", -1) !== "test");
assert(getHash("test", 0) !== "test");
assert(getHash("test", null) !== "test");
});
it('Should return a hashed value', () => {
assert.equal(getHash("test"), "2f327ef967ade1ebf4319163f7debbda9cc17bb0c8c834b00b30ca1cf1c256ee");
});
it ('Should be able to output the same has the DB upgrade script will output', () => {
assert.equal(getHash("vid", 1), "1ff838dc6ca9680d88455341118157d59a055fe6d0e3870f9c002847bebe4663");
});
it ('Should take a variable number of passes', () => {
assert.equal(getHash("test", 1), "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
assert.equal(getHash("test", 2), "7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c");
assert.equal(getHash("test", 3), "5b24f7aa99f1e1da5698a4f91ae0f4b45651a1b625c61ed669dd25ff5b937972");
});
it ('Should default to 5000 passes', () => {
assert.equal(getHash("test"), getHash("test", 5000));
});
it ('Should not take a negative number of passes', () => {
assert.equal(getHash("test", -1), "");
});
});

View File

@@ -0,0 +1,57 @@
var request = require('request');
var utils = require('../utils.js');
var db = require('../../src/databases/databases.js').db;
var getHash = require('../../src/utils/getHash.js');
describe('getIsUserVIP', () => {
before(() => {
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("supertestman") + "')");
});
it('Should be able to get a 200', (done) => {
request.get(utils.getbaseURL()
+ "/api/isUserVIP?userID=supertestman", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200: " + res.statusCode);
else done(); // pass
});
});
it('Should get a 400 if no userID', (done) => {
request.get(utils.getbaseURL()
+ "/api/isUserVIP", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 400) done("non 400: " + res.statusCode);
else done(); // pass
});
});
it('Should say a VIP is a VIP', (done) => {
request.get(utils.getbaseURL()
+ "/api/isUserVIP?userID=supertestman", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200: " + res.statusCode);
else {
if (JSON.parse(body).vip === true) done(); // pass
else done("Result was non-vip when should have been vip");
}
});
});
it('Should say a normal user is not a VIP', (done) => {
request.get(utils.getbaseURL()
+ "/api/isUserVIP?userID=regulartestman", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200: " + res.statusCode);
else {
if (JSON.parse(body).vip === false) done(); // pass
else done("Result was vip when should have been non-vip");
}
});
});
});

View File

@@ -0,0 +1,21 @@
var request = require('request');
var utils = require('../utils.js');
var db = require('../../src/databases/databases.js').db;
var getHash = require('../../src/utils/getHash.js');
describe('getSavedTimeForUser', () => {
before(() => {
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfQuery + "('getSavedTimeForUser', 1, 11, 2, 'abc1239999', '" + getHash("testman") + "', 0, 50, 'sponsor', 0, '" + getHash('getSavedTimeForUser', 0) + "')");
});
it('Should be able to get a 200', (done) => {
request.get(utils.getbaseURL()
+ "/api/getSavedTimeForUser?userID=testman", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200");
else done(); // pass
});
});
});

View File

@@ -0,0 +1,190 @@
var request = require('request');
var db = require('../../src/databases/databases.js').db;
var utils = require('../utils.js');
var getHash = require('../../src/utils/getHash.js');
describe('getSegmentsByHash', () => {
before(() => {
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfQuery + "('getSegmentsByHash-0', 1, 10, 2, 'getSegmentsByHash-0-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
db.exec(startOfQuery + "('getSegmentsByHash-0', 20, 30, 2, 'getSegmentsByHash-0-1', 'testman', 100, 150, 'intro', 0, '" + getHash('getSegmentsByHash-0', 1) + "')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
db.exec(startOfQuery + "('getSegmentsByHash-noMatchHash', 40, 50, 2, 'getSegmentsByHash-noMatchHash', 'testman', 0, 50, 'sponsor', 0, 'fdaffnoMatchHash')"); // hash = fdaff4dee1043451faa7398324fb63d8618ebcd11bddfe0491c488db12c6c910
db.exec(startOfQuery + "('getSegmentsByHash-1', 60, 70, 2, 'getSegmentsByHash-1', 'testman', 0, 50, 'sponsor', 0, '" + getHash('getSegmentsByHash-1', 1) + "')"); // hash = 3272fa85ee0927f6073ef6f07ad5f3146047c1abba794cfa364d65ab9921692b
});
it('Should be able to get a 200', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/3272f?categories=["sponsor", "intro"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200 status code, was " + res.statusCode);
else {
done();
} // pass
});
});
it('Should be able to get a 200 with empty segments for video but no matching categories', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/3272f?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200 status code, was " + res.statusCode);
else {
if (JSON.parse(body) && JSON.parse(body).length > 0 && JSON.parse(body)[0].segments.length === 0) {
done(); // pass
} else {
done("response had segments");
}
}
});
});
it('Should be able to get an empty array if no videos', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/11111?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 404) done("non 404 status code, was " + res.statusCode);
else {
if (JSON.parse(body).length === 0 && body === '[]') done(); // pass
else done("non empty array returned");
}
});
});
it('Should return 400 prefix too short', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/11?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 400) done("non 400 status code, was " + res.statusCode);
else {
done(); // pass
}
});
});
it('Should return 400 prefix too long', (done) => {
let prefix = new Array(50).join('1');
if (prefix.length <= 32) { // default value, config can change this
done('failed to generate a long enough string for the test ' + prefix.length);
return;
}
request.get(utils.getbaseURL()
+ '/api/skipSegments/'+prefix+'?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 400) done("non 400 status code, was " + res.statusCode);
else {
done(); // pass
}
});
});
it('Should not return 400 prefix in range', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/11111?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 400) done("prefix length 5 gave 400 " + res.statusCode);
else {
done(); // pass
}
});
});
it('Should return 404 for no hash', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/?categories=["shilling"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 404) done("expected 404, got " + res.statusCode);
else {
done(); // pass
}
});
});
it('Should return 500 for bad format categories', (done) => { // should probably be 400
request.get(utils.getbaseURL()
+ '/api/skipSegments/?categories=shilling', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 500) done("expected 500 got " + res.statusCode);
else {
done(); // pass
}
});
});
it('Should be able to get multiple videos', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/fdaf?categories=["sponsor","intro"]', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200 status code, was " + res.statusCode);
else {
body = JSON.parse(body);
if (body.length !== 2) done("expected 2 video, got " + body.length);
else if (body[0].segments.length !== 2) done("expected 2 segments for first video, got " + body[0].segments.length);
else if (body[1].segments.length !== 1) done("expected 1 segment for second video, got " + body[1].segments.length);
else done();
}
});
});
it('Should be able to get 200 for no categories (default sponsor)', (done) => {
request.get(utils.getbaseURL()
+ '/api/skipSegments/fdaf', null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200 status code, was " + res.statusCode);
else {
body = JSON.parse(body);
if (body.length !== 2) done("expected 2 videos, got " + body.length);
else if (body[0].segments.length !== 1) done("expected 1 segments for first video, got " + body[0].segments.length);
else if (body[1].segments.length !== 1) done("expected 1 segments for second video, got " + body[1].segments.length);
else if (body[0].segments[0].category !== 'sponsor' || body[1].segments[0].category !== 'sponsor') done("both segments are not sponsor");
else done();
}
});
});
it('Should be able to post a segment and get it using endpoint', (done) => {
let testID = 'abc123goodVideo';
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: testID,
segments: [{
segment: [13, 17],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done('(post) ' + err);
else if (res.statusCode === 200) {
request.get(utils.getbaseURL()
+ '/api/skipSegments/'+getHash(testID, 1).substring(0,3), null,
(err, res, body) => {
if (err) done("(get) Couldn't call endpoint");
else if (res.statusCode !== 200) done("(get) non 200 status code, was " + res.statusCode);
else {
body = JSON.parse(body);
if (body.length !== 1) done("(get) expected 1 video, got " + body.length);
else if (body[0].segments.length !== 1) done("(get) expected 1 segments for first video, got " + body[0].segments.length);
else if (body[0].segments[0].category !== 'sponsor') done("(get) segment should be sponsor, was "+body[0].segments[0].category);
else done();
}
});
} else {
done("(post) non 200 status code, was " + res.statusCode);
}
}
);
});
});

View File

@@ -0,0 +1,217 @@
var request = require('request');
var db = require('../../src/databases/databases.js').db;
var utils = require('../utils.js');
var getHash = require('../../src/utils/getHash.js');
describe('getSkipSegments', () => {
before(() => {
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfQuery + "('testtesttest', 1, 11, 2, '1-uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('testtesttest', 1) + "')");
db.exec(startOfQuery + "('testtesttest', 20, 33, 2, '1-uuid-2', 'testman', 0, 50, 'intro', 0, '" + getHash('testtesttest', 1) + "')");
db.exec(startOfQuery + "('testtesttest,test', 1, 11, 2, '1-uuid-1', 'testman', 0, 50, 'sponsor', 0, '" + getHash('testtesttest,test', 1) + "')");
db.exec(startOfQuery + "('test3', 1, 11, 2, '1-uuid-4', 'testman', 0, 50, 'sponsor', 0, '" + getHash('test3', 1) + "')");
db.exec(startOfQuery + "('test3', 7, 22, -3, '1-uuid-5', 'testman', 0, 50, 'sponsor', 0, '" + getHash('test3', 1) + "')");
db.exec(startOfQuery + "('multiple', 1, 11, 2, '1-uuid-6', 'testman', 0, 50, 'intro', 0, '" + getHash('multiple', 1) + "')");
db.exec(startOfQuery + "('multiple', 20, 33, 2, '1-uuid-7', 'testman', 0, 50, 'intro', 0, '" + getHash('multiple', 1) + "')");
});
it('Should be able to get a time by category 1', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get a time by category 2', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&category=intro", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 20 && data[0].segment[1] === 33
&& data[0].category === "intro" && data[0].UUID === "1-uuid-2") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get a time by categories array', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&categories=[\"sponsor\"]", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get a time by categories array 2', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&categories=[\"intro\"]", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 20 && data[0].segment[1] === 33
&& data[0].category === "intro" && data[0].UUID === "1-uuid-2") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get multiple times by category', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=multiple&categories=[\"intro\"]", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 2) {
let success = true;
for (const segment of data) {
if ((segment.segment[0] !== 20 || segment.segment[1] !== 33
|| segment.category !== "intro" || segment.UUID !== "1-uuid-7") &&
(segment.segment[0] !== 1 || segment.segment[1] !== 11
|| segment.category !== "intro" || segment.UUID !== "1-uuid-6")) {
success = false;
break;
}
}
if (success) done();
else done("Received incorrect body: " + res.body);
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get multiple times by multiple categories', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&categories=[\"sponsor\", \"intro\"]", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 2) {
let success = true;
for (const segment of data) {
if ((segment.segment[0] !== 20 || segment.segment[1] !== 33
|| segment.category !== "intro" || segment.UUID !== "1-uuid-2") &&
(segment.segment[0] !== 1 || segment.segment[1] !== 11
|| segment.category !== "sponsor" || segment.UUID !== "1-uuid-0")) {
success = false;
break;
}
}
if (success) done();
else done("Received incorrect body: " + res.body);
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be possible to send unexpected query parameters', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&fakeparam=hello&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Low voted submissions should be hidden', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=test3&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-4") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should return 404 if no segment found', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=notarealvideo", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 404) done("non 404 respone code: " + res.statusCode);
else done(); // pass
});
});
it('Should be able send a comma in a query param', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest,test&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-1") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
});

View File

@@ -0,0 +1,8 @@
const getSubmissionUUID = require('../../src/utils/getSubmissionUUID.js');
const assert = require('assert');
describe('getSubmissionUUID', () => {
it('Should return the hashed value', () => {
assert.equal(getSubmissionUUID('video001', 'sponsor', 'testuser001', 13.33337, 42.000001), '1d33d7016aa6482849019bd906d75c08fe6b815e64e823146df35f66c35612dd');
});
});

167
test/cases/getUserInfo.js Normal file
View File

@@ -0,0 +1,167 @@
var request = require('request');
var utils = require('../utils.js');
var db = require('../../src/databases/databases.js').db;
var getHash = require('../../src/utils/getHash.js');
describe('getUserInfo', () => {
before(() => {
let startOfUserNamesQuery = "INSERT INTO userNames (userID, userName) VALUES";
db.exec(startOfUserNamesQuery + "('" + getHash("getuserinfo_user_01") + "', 'Username user 01')");
let startOfSponsorTimesQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden) VALUES";
db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000001', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)");
db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000002', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)");
db.exec(startOfSponsorTimesQuery + "('yyyxxxzzz', 1, 11, -1, 'uuid000003', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)");
db.exec(startOfSponsorTimesQuery + "('yyyxxxzzz', 1, 11, -2, 'uuid000004', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 1)");
db.exec(startOfSponsorTimesQuery + "('xzzzxxyyy', 1, 11, -5, 'uuid000005', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 1)");
db.exec(startOfSponsorTimesQuery + "('zzzxxxyyy', 1, 11, 2, 'uuid000006', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 0)");
db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000007', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)");
db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000008', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)");
db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_0') + "', 10, 'getuserinfo_vip')");
db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_1') + "', 10, 'getuserinfo_vip')");
db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_1') + "', 10, 'getuserinfo_vip')");
});
it('Should be able to get a 200', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_user_01', null,
(err, res, body) => {
if (err) {
done('couldn\'t call endpoint');
} else {
if (res.statusCode !== 200) {
done('non 200 (' + res.statusCode + ')');
} else {
done(); // pass
}
}
});
});
it('Should be able to get a 400 (No userID parameter)', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo', null,
(err, res, body) => {
if (err) {
done('couldn\'t call endpoint');
} else {
if (res.statusCode !== 400) {
done('non 400');
} else {
done(); // pass
}
}
});
});
it('Should return info', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_user_01', null,
(err, res, body) => {
if (err) {
done("couldn't call endpoint");
} else {
if (res.statusCode !== 200) {
done("non 200");
} else {
const data = JSON.parse(body);
if (data.userName !== 'Username user 01') {
done('Returned incorrect userName "' + data.userName + '"');
} else if (data.minutesSaved !== 5) {
done('Returned incorrect minutesSaved "' + data.minutesSaved + '"');
} else if (data.viewCount !== 30) {
done('Returned incorrect viewCount "' + data.viewCount + '"');
} else if (data.segmentCount !== 3) {
done('Returned incorrect segmentCount "' + data.segmentCount + '"');
} else {
done(); // pass
}
}
}
});
});
it('Should get warning data', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_warning_0', null,
(err, res, body) => {
if (err) {
done("couldn't call endpoint");
} else {
if (res.statusCode !== 200) {
done("non 200");
} else {
const data = JSON.parse(body);
if (data.warnings !== 1) {
done('wrong number of warnings: ' + data.warnings + ', not ' + 1);
} else {
done(); // pass
}
}
}
});
});
it('Should get multiple warnings', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_warning_1', null,
(err, res, body) => {
if (err) {
done("couldn't call endpoint");
} else {
if (res.statusCode !== 200) {
done("non 200");
} else {
const data = JSON.parse(body);
if (data.warnings !== 2) {
done('wrong number of warnings: ' + data.warnings + ', not ' + 2);
} else {
done(); // pass
}
}
}
});
});
it('Should not get warnings if noe', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_warning_2', null,
(err, res, body) => {
if (err) {
done("couldn't call endpoint");
} else {
if (res.statusCode !== 200) {
done("non 200");
} else {
const data = JSON.parse(body);
if (data.warnings !== 0) {
done('wrong number of warnings: ' + data.warnings + ', not ' + 0);
} else {
done(); // pass
}
}
}
});
});
it('Should return userID for userName (No userName set)', (done) => {
request.get(utils.getbaseURL()
+ '/api/getUserInfo?userID=getuserinfo_user_02', null,
(err, res, body) => {
if (err) {
done('couldn\'t call endpoint');
} else {
if (res.statusCode !== 200) {
done('non 200');
} else {
const data = JSON.parse(body);
if (data.userName !== 'c2a28fd225e88f74945794ae85aef96001d4a1aaa1022c656f0dd48ac0a3ea0f') {
return done('Did not return userID for userName');
}
done(); // pass
}
}
});
});
});

View File

@@ -0,0 +1,466 @@
var request = require('request');
var utils = require('../utils.js');
const getHash = require('../../src/utils/getHash.js');
var databases = require('../../src/databases/databases.js');
const logger = require('../../src/utils/logger.js');
var db = databases.db;
describe('noSegmentRecords', () => {
before(() => {
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("VIPUser-noSegments") + "')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'no-segments-video-id', 'sponsor')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'no-segments-video-id', 'intro')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'no-segments-video-id-1', 'sponsor')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'no-segments-video-id-1', 'intro')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'noSubmitVideo', 'sponsor')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'delete-record', 'sponsor')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'delete-record-1', 'sponsor')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-noSegments") + "', 'delete-record-1', 'intro')");
});
it('Should update the database version when starting the application', (done) => {
let version = db.prepare('get', 'SELECT key, value FROM config where key = ?', ['version']).value;
if (version > 1) done();
else done('Version isn\'t greater than 1. Version is ' + version);
});
it('Should be able to submit categories not in video (http response)', (done) => {
let json = {
videoID: 'no-segments-video-id',
userID: 'VIPUser-noSegments',
categories: [
'outro',
'shilling',
'shilling',
'shil ling',
'',
'intro'
]
};
let expected = {
submitted: [
'outro',
'shilling'
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
if (JSON.stringify(body) === JSON.stringify(expected)) {
done();
} else {
done("Incorrect response: expected " + JSON.stringify(expected) + " got " + JSON.stringify(body));
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit categories not in video (sql check)', (done) => {
let json = {
videoID: 'no-segments-video-id-1',
userID: 'VIPUser-noSegments',
categories: [
'outro',
'shilling',
'shilling',
'shil ling',
'',
'intro'
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['no-segments-video-id-1']);
if (result.length !== 4) {
console.log(result);
done("Expected 4 entrys in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit categories with _ in the category', (done) => {
let json = {
videoID: 'underscore',
userID: 'VIPUser-noSegments',
categories: [
'word_word',
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['underscore']);
if (result.length !== 1) {
console.log(result);
done("Expected 1 entrys in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit categories with upper and lower case in the category', (done) => {
let json = {
videoID: 'bothCases',
userID: 'VIPUser-noSegments',
categories: [
'wordWord',
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['bothCases']);
if (result.length !== 1) {
console.log(result);
done("Expected 1 entrys in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to submit categories with $ in the category', (done) => {
let json = {
videoID: 'specialChar',
userID: 'VIPUser-noSegments',
categories: [
'word&word',
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['specialChar']);
if (result.length !== 0) {
console.log(result);
done("Expected 0 entrys in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for missing params', (done) => {
request.post(utils.getbaseURL()
+ "/api/noSegments", {json: {}},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for no categories', (done) => {
let json = {
videoID: 'test',
userID: 'test',
categories: []
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for no userID', (done) => {
let json = {
videoID: 'test',
userID: null,
categories: ['sponsor']
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for no videoID', (done) => {
let json = {
videoID: null,
userID: 'test',
categories: ['sponsor']
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 object categories)', (done) => {
let json = {
videoID: 'test',
userID: 'test',
categories: {}
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 bad format categories', (done) => {
let json = {
videoID: 'test',
userID: 'test',
categories: 'sponsor'
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 403 if user is not VIP', (done) => {
let json = {
videoID: 'test',
userID: 'test',
categories: [
'sponsor'
]
};
request.post(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done();
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to delete a noSegment record', (done) => {
let json = {
videoID: 'delete-record',
userID: 'VIPUser-noSegments',
categories: [
'sponsor'
]
};
request.delete(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['delete-record']);
if (result.length === 0) {
done();
} else {
done("Didn't delete record");
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to delete one noSegment record without removing another', (done) => {
let json = {
videoID: 'delete-record-1',
userID: 'VIPUser-noSegments',
categories: [
'sponsor'
]
};
request.delete(utils.getbaseURL()
+ "/api/noSegments", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM noSegments WHERE videoID = ?', ['delete-record-1']);
if (result.length === 1) {
done();
} else {
done("Didn't delete record");
}
} else {
done("Status code was " + res.statusCode);
}
});
});
/*
* Submission tests in this file do not check database records, only status codes.
* To test the submission code properly see ./test/cases/postSkipSegments.js
*/
it('Should not be able to submit a segment to a video with a no-segment record (single submission)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "testman42",
videoID: "noSubmitVideo",
segments: [{
segment: [20, 40],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to submit segments to a video where any of the submissions with a no-segment record', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "testman42",
videoID: "noSubmitVideo",
segments: [{
segment: [20, 40],
category: "sponsor"
},{
segment: [50, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a segment to a video with a different no-segment record', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "testman42",
videoID: "noSubmitVideo",
segments: [{
segment: [20, 40],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a segment to a video with no no-segment records', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "testman42",
videoID: "normalVideo",
segments: [{
segment: [20, 40],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
done()
} else {
done("Status code was " + res.statusCode);
}
});
});
});

View File

@@ -0,0 +1,88 @@
var request = require('request');
var db = require('../../src/databases/databases.js').db;
var utils = require('../utils.js');
var getHash = require('../../src/utils/getHash.js');
/*
*CREATE TABLE IF NOT EXISTS "sponsorTimes" (
"videoID" TEXT NOT NULL,
"startTime" REAL NOT NULL,
"endTime" REAL NOT NULL,
"votes" INTEGER NOT NULL,
"UUID" TEXT NOT NULL UNIQUE,
"userID" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL,
"views" INTEGER NOT NULL,
"shadowHidden" INTEGER NOT NULL
);
*/
describe('getVideoSponsorTime (Old get method)', () => {
before(() => {
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfQuery + "('old-testtesttest', 1, 11, 2, 'uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('old-testtesttest', 1) + "')");
db.exec(startOfQuery + "('old-testtesttest,test', 1, 11, 2, 'uuid-1', 'testman', 0, 50, 'sponsor', 0, '" + getHash('old-testtesttest,test', 1) + "')");
});
it('Should be able to get a time', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200");
else done(); // pass
});
});
it('Should return 404 if no segment found', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=notarealvideo", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 404) done("non 404 respone code: " + res.statusCode);
else done(); // pass
});
});
it('Should be possible to send unexpected query parameters', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest&fakeparam=hello", null,
(err, res, body) => {
if (err) done("couldn't callendpoint");
else if (res.statusCode !== 200) done("non 200");
else done(); // pass
});
});
it('Should be able send a comma in a query param', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest,test", null,
(err, res, body) => {
if (err) done("couln't call endpoint");
else if (res.statusCode !== 200) done("non 200 response: " + res.statusCode);
else if (JSON.parse(body).UUIDs[0] === 'uuid-1') done(); // pass
else done("couldn't parse response");
});
});
it('Should be able to get the correct time', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200");
else {
let parsedBody = JSON.parse(body);
if (parsedBody.sponsorTimes[0][0] === 1
&& parsedBody.sponsorTimes[0][1] === 11
&& parsedBody.UUIDs[0] === 'uuid-0') {
done(); // pass
} else {
done("Wrong data was returned + " + parsedBody);
}
};
});
});
});

View File

@@ -0,0 +1,55 @@
var assert = require('assert');
var request = require('request');
var utils = require('../utils.js');
var databases = require('../../src/databases/databases.js');
var db = databases.db;
describe('postVideoSponsorTime (Old submission method)', () => {
it('Should be able to submit a time (GET)', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcQ&startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?", ["dQw4w9WgXcQ"]);
if (row.startTime === 1 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a time (POST)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcE&startTime=1&endTime=11&userID=test", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?", ["dQw4w9WgXcE"]);
if (row.startTime === 1 && row.endTime === 11 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for missing params', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(err);
if (res.statusCode === 400) done();
else done("Status code was: " + res.statusCode);
});
});
});

View File

@@ -0,0 +1,455 @@
var assert = require('assert');
var request = require('request');
var config = require('../../src/config.js');
var getHash = require('../../src/utils/getHash.js');
var utils = require('../utils.js');
var databases = require('../../src/databases/databases.js');
var db = databases.db;
describe('postSkipSegments', () => {
before(() => {
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfQuery + "('80percent_video', 0, 1000, 0, '80percent-uuid-0', '" + getHash("test") + "', 0, 0, 'interaction', 0, '80percent_video')");
db.exec(startOfQuery + "('80percent_video', 1001, 1005, 0, '80percent-uuid-1', '" + getHash("test") + "', 0, 0, 'interaction', 0, '80percent_video')");
db.exec(startOfQuery + "('80percent_video', 0, 5000, -2, '80percent-uuid-2', '" + getHash("test") + "', 0, 0, 'interaction', 0, '80percent_video')");
const now = Date.now();
const warnVip01Hash = getHash("warn-vip01");
const warnUser01Hash = getHash("warn-user01");
const warnUser02Hash = getHash("warn-user02");
const MILLISECONDS_IN_HOUR = 3600000;
const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires;
const startOfWarningQuery = 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES';
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-1000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-2000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-3601000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 1000)) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 2000)) + "', '" + warnVip01Hash + "')");
});
it('Should be able to submit a single time (Params method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcR&startTime=2&endTime=10&userID=test&category=sponsor", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?", ["dQw4w9WgXcR"]);
if (row.startTime === 2 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a single time (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcF",
segments: [{
segment: [0, 10],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?", ["dQw4w9WgXcF"]);
if (row.startTime === 0 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit multiple times (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [3, 10],
category: "sponsor"
}, {
segment: [30, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let rows = db.prepare('all', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?", ["dQw4w9WgXcR"]);
let success = true;
if (rows.length === 2) {
for (const row of rows) {
if ((row.startTime !== 3 || row.endTime !== 10 || row.category !== "sponsor") &&
(row.startTime !== 30 || row.endTime !== 60 || row.category !== "intro")) {
success = false;
break;
}
}
}
if (success) done();
else done("Submitted times were not saved. Actual submissions: " + JSON.stringify(row));
} else {
done("Status code was " + res.statusCode);
}
});
}).timeout(5000);
it('Should allow multiple times if total is under 80% of video(JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "L_jWHffIx5E",
segments: [{
segment: [3, 3000],
category: "sponsor"
},{
segment: [3002, 3050],
category: "intro"
},{
segment: [45, 100],
category: "interaction"
},{
segment: [99, 170],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let rows = db.prepare('all', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ? and votes > -1", ["L_jWHffIx5E"]);
let success = true;
if (rows.length === 4) {
for (const row of rows) {
if ((row.startTime !== 3 || row.endTime !== 3000 || row.category !== "sponsor") &&
(row.startTime !== 3002 || row.endTime !== 3050 || row.category !== "intro") &&
(row.startTime !== 45 || row.endTime !== 100 || row.category !== "interaction") &&
(row.startTime !== 99 || row.endTime !== 170 || row.category !== "sponsor")) {
success = false;
break;
}
}
}
if (success) done();
else done("Submitted times were not saved. Actual submissions: " + JSON.stringify(rows));
} else {
done("Status code was " + res.statusCode);
}
});
}).timeout(5000);
it('Should reject multiple times if total is over 80% of video (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "n9rIGdXnSJc",
segments: [{
segment: [0, 2000],
category: "interaction"
},{
segment: [3000, 4000],
category: "sponsor"
},{
segment: [1500, 2750],
category: "sponsor"
},{
segment: [4050, 4750],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
let rows = db.prepare('all', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ? and votes > -1", ["n9rIGdXnSJc"]);
let success = true;
if (rows.length === 4) {
for (const row of rows) {
if ((row.startTime === 0 || row.endTime === 2000 || row.category === "interaction") ||
(row.startTime === 3000 || row.endTime === 4000 || row.category === "sponsor") ||
(row.startTime === 1500 || row.endTime === 2750 || row.category === "sponsor") ||
(row.startTime === 4050 || row.endTime === 4750 || row.category === "intro")) {
success = false;
break;
}
}
}
if (success) done();
else
done("Submitted times were not saved. Actual submissions: " + JSON.stringify(rows));
} else {
done("Status code was " + res.statusCode);
}
});
}).timeout(5000);
it('Should reject multiple times if total is over 80% of video including previosuly submitted times(JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "80percent_video",
segments: [{
segment: [2000, 4000],
category: "sponsor"
},{
segment: [1500, 2750],
category: "sponsor"
},{
segment: [4050, 4750],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
let rows = db.prepare('all', "SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ? and votes > -1", ["80percent_video"]);
let success = true && rows.length == 2;
for (const row of rows) {
if ((row.startTime === 2000 || row.endTime === 4000 || row.category === "sponsor") ||
(row.startTime === 1500 || row.endTime === 2750 || row.category === "sponsor") ||
(row.startTime === 4050 || row.endTime === 4750 || row.category === "sponsor")) {
success = false;
break;
}
}
if (success) done();
else
done("Submitted times were not saved. Actual submissions: " + JSON.stringify(rows));
} else {
done("Status code was " + res.statusCode);
}
});
}).timeout(5000);
it('Should be accepted if a non-sponsor is less than 1 second', (done) => {
request.post(utils.getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30.5&userID=testing&category=intro", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 200) done(); // pass
else done("non 200 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if a sponsor is less than 1 second', (done) => {
request.post(utils.getbaseURL()
+ "/api/skipSegments?videoID=qqwerty&startTime=30&endTime=30.5&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 400) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if over 80% of the video', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=qqwerty&startTime=30&endTime=1000000&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it("Should be rejected if NB's predicted probability is <70%.", (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=LevkAjUE6d4&startTime=40&endTime=60&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 200) done(); // pass
else done("non 200 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if user has to many active warnings', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "warn-user01",
videoID: "dQw4w9WgXcF",
segments: [{
segment: [0, 10],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done(); // success
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be accepted if user has some active warnings', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "warn-user02",
videoID: "dQw4w9WgXcF",
segments: [{
segment: [50, 60],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
done(); // success
} else {
done("Status code was " + res.statusCode + " " + body);
}
});
});
it('Should be allowed if youtube thinks duration is 0', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=noDuration&startTime=30&endTime=10000&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 200) done(); // pass
else done("non 200 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if not a valid videoID', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=knownWrongID&startTime=30&endTime=1000000&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should return 400 for missing params (Params method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=9&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(true);
if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 1', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
segments: [{
segment: [9, 10],
category: "sponsor"
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 2', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ"
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 3', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [0],
category: "sponsor"
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 4', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [9, 10]
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 5', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ"
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
});

47
test/cases/postWarning.js Normal file
View File

@@ -0,0 +1,47 @@
var request = require('request');
var utils = require('../utils.js');
var db = require('../../src/databases/databases.js').db;
var getHash = require('../../src/utils/getHash.js');
describe('postWarning', () => {
before(() => {
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("warning-vip") + "')");
});
it('Should be able to create warning if vip (exp 200)', (done) => {
let json = {
issuerUserID: 'warning-vip',
userID: 'warning-0'
};
request.post(utils.getbaseURL()
+ "/api/warnUser", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
done();
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to create warning if vip (exp 403)', (done) => {
let json = {
issuerUserID: 'warning-not-vip',
userID: 'warning-1'
};
request.post(utils.getbaseURL()
+ "/api/warnUser", {json},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done();
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
});

273
test/cases/segmentShift.js Normal file
View File

@@ -0,0 +1,273 @@
const request = require('request');
const utils = require('../utils.js');
const { db } = require('../../src/databases/databases.js');
const getHash = require('../../src/utils/getHash.js');
function dbSponsorTimesAdd(db, videoID, startTime, endTime, UUID, category) {
const votes = 0,
userID = 0,
timeSubmitted = 0,
views = 0,
shadowHidden = 0,
hashedVideoID = `hash_${UUID}`;
db.exec(`INSERT INTO
sponsorTimes (videoID, startTime, endTime, votes, UUID,
userID, timeSubmitted, views, category, shadowHidden, hashedVideoID)
VALUES
('${videoID}', ${startTime}, ${endTime}, ${votes}, '${UUID}',
'${userID}', ${timeSubmitted}, ${views}, '${category}', ${shadowHidden}, '${hashedVideoID}')
`);
}
function dbSponsorTimesSetByUUID(db, UUID, startTime, endTime) {
db.prepare('run', `UPDATE sponsorTimes SET startTime = ?, endTime = ? WHERE UUID = ?`, [startTime, endTime, UUID]);
}
function dbSponsorTimesCompareExpect(db, expect) {
for (let i=0, len=expect.length; i<len; i++) {
const expectSeg = expect[i];
let seg = db.prepare('get', "SELECT startTime, endTime FROM sponsorTimes WHERE UUID = ?", [expectSeg.UUID]);
if ('removed' in expect) {
if (expect.removed === true && seg.votes === -2) {
return;
}
else {
return `${expectSeg.UUID} doesnt got removed`;
}
}
if (seg.startTime !== expectSeg.startTime) {
return `${expectSeg.UUID} startTime is incorrect. seg.startTime is ${seg.startTime} expected ${expectSeg.startTime}`;
}
if (seg.endTime !== expectSeg.endTime) {
return `${expectSeg.UUID} endTime is incorrect. seg.endTime is ${seg.endTime} expected ${expectSeg.endTime}`;
}
}
return;
}
describe('segmentShift', function() {
const privateVipUserID = 'VIPUser-segmentShift';
const vipUserID = getHash(privateVipUserID);
const baseURL = utils.getbaseURL();
before(function(done) {
// startTime and endTime get set in beforeEach for consistency
dbSponsorTimesAdd(db, 'vsegshift01', 0, 0, 'vsegshifttest01uuid01', 'intro');
dbSponsorTimesAdd(db, 'vsegshift01', 0, 0, 'vsegshifttest01uuid02', 'sponsor');
dbSponsorTimesAdd(db, 'vsegshift01', 0, 0, 'vsegshifttest01uuid03', 'interaction');
dbSponsorTimesAdd(db, 'vsegshift01', 0, 0, 'vsegshifttest01uuid04', 'outro');
db.exec(`INSERT INTO vipUsers (userID) VALUES ('${vipUserID}')`);
done();
});
beforeEach(function(done) {
// resetting startTime and endTime to reuse them
dbSponsorTimesSetByUUID(db, 'vsegshifttest01uuid01', 0, 10);
dbSponsorTimesSetByUUID(db, 'vsegshifttest01uuid02', 60, 90);
dbSponsorTimesSetByUUID(db, 'vsegshifttest01uuid03', 40, 45);
dbSponsorTimesSetByUUID(db, 'vsegshifttest01uuid04', 120, 140);
done();
});
it('Reject none VIP user', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: 'segshift_randomuser001',
startTime: 20,
endTime: 30,
}
}, (err, res, body) => {
if (err) return done(err);
return done(res.statusCode === 403 ? undefined : res.statusCode);
});
});
it('Shift is outside segments', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: privateVipUserID,
startTime: 20,
endTime: 30,
}
}, (err, res, body) => {
if (err) return done(err);
if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`);
const expect = [
{
UUID: 'vsegshifttest01uuid01',
startTime: 0,
endTime: 10,
},
{
UUID: 'vsegshifttest01uuid02',
startTime: 50,
endTime: 80,
},
{
UUID: 'vsegshifttest01uuid03',
startTime: 30,
endTime: 35,
},
{
UUID: 'vsegshifttest01uuid04',
startTime: 110,
endTime: 130,
},
];
done(dbSponsorTimesCompareExpect(db, expect));
});
});
it('Shift is inside segment', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: privateVipUserID,
startTime: 65,
endTime: 75,
}
}, (err, res, body) => {
if (err) return done(err);
if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`);
const expect = [
{
UUID: 'vsegshifttest01uuid01',
startTime: 0,
endTime: 10,
},
{
UUID: 'vsegshifttest01uuid02',
startTime: 60,
endTime: 80,
},
{
UUID: 'vsegshifttest01uuid03',
startTime: 40,
endTime: 45,
},
{
UUID: 'vsegshifttest01uuid04',
startTime: 110,
endTime: 130,
},
];
done(dbSponsorTimesCompareExpect(db, expect));
});
});
it('Shift is overlaping startTime of segment', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: privateVipUserID,
startTime: 32,
endTime: 42,
}
}, (err, res, body) => {
if (err) return done(err);
if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`);
const expect = [
{
UUID: 'vsegshifttest01uuid01',
startTime: 0,
endTime: 10,
},
{
UUID: 'vsegshifttest01uuid02',
startTime: 50,
endTime: 80,
},
{
UUID: 'vsegshifttest01uuid03',
startTime: 32,
endTime: 35,
},
{
UUID: 'vsegshifttest01uuid04',
startTime: 110,
endTime: 130,
},
];
done(dbSponsorTimesCompareExpect(db, expect));
});
});
it('Shift is overlaping endTime of segment', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: privateVipUserID,
startTime: 85,
endTime: 95,
}
}, (err, res, body) => {
if (err) return done(err);
if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`);
const expect = [
{
UUID: 'vsegshifttest01uuid01',
startTime: 0,
endTime: 10,
},
{
UUID: 'vsegshifttest01uuid02',
startTime: 60,
endTime: 85,
},
{
UUID: 'vsegshifttest01uuid03',
startTime: 40,
endTime: 45,
},
{
UUID: 'vsegshifttest01uuid04',
startTime: 110,
endTime: 130,
},
];
done(dbSponsorTimesCompareExpect(db, expect));
});
});
it('Shift is overlaping segment', function(done) {
request.post(`${baseURL}/api/segmentShift`, {
json: {
videoID: 'vsegshift01',
userID: privateVipUserID,
startTime: 35,
endTime: 55,
}
}, (err, res, body) => {
if (err) return done(err);
if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`);
const expect = [
{
UUID: 'vsegshifttest01uuid01',
startTime: 0,
endTime: 10,
},
{
UUID: 'vsegshifttest01uuid02',
startTime: 40,
endTime: 70,
},
{
UUID: 'vsegshifttest01uuid03',
startTime: 40,
endTime: 45,
removed: true,
},
{
UUID: 'vsegshifttest01uuid04',
startTime: 100,
endTime: 120,
},
];
done(dbSponsorTimesCompareExpect(db, expect));
});
});
});

79
test/cases/unBan.js Normal file
View File

@@ -0,0 +1,79 @@
var request = require('request');
var utils = require('../utils.js');
const getHash = require('../../src/utils/getHash.js');
var databases = require('../../src/databases/databases.js');
const logger = require('../../src/utils/logger.js');
var db = databases.db;
describe('unBan', () => {
before(() => {
db.exec("INSERT INTO shadowBannedUsers VALUES('testMan-unBan')");
db.exec("INSERT INTO shadowBannedUsers VALUES('testWoman-unBan')");
db.exec("INSERT INTO shadowBannedUsers VALUES('testEntity-unBan')");
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("VIPUser-unBan") + "')");
db.exec("INSERT INTO noSegments (userID, videoID, category) VALUES ('" + getHash("VIPUser-unBan") + "', 'unBan-videoID-1', 'sponsor')");
let startOfInsertSegmentQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
db.exec(startOfInsertSegmentQuery + "('unBan-videoID-0', 1, 11, 2, 'unBan-uuid-0', 'testMan-unBan', 0, 50, 'sponsor', 1, '" + getHash('unBan-videoID-0', 1) + "')");
db.exec(startOfInsertSegmentQuery + "('unBan-videoID-1', 1, 11, 2, 'unBan-uuid-1', 'testWoman-unBan', 0, 50, 'sponsor', 1, '" + getHash('unBan-videoID-1', 1) + "')");
db.exec(startOfInsertSegmentQuery + "('unBan-videoID-1', 1, 11, 2, 'unBan-uuid-2', 'testEntity-unBan', 0, 60, 'sponsor', 1, '" + getHash('unBan-videoID-1', 1) + "')");
db.exec(startOfInsertSegmentQuery + "('unBan-videoID-2', 1, 11, 2, 'unBan-uuid-3', 'testEntity-unBan', 0, 60, 'sponsor', 1, '" + getHash('unBan-videoID-2', 1) + "')");
});
it('Should be able to unban a user and re-enable shadow banned segments', (done) => {
request.post(utils.getbaseURL() + "/api/shadowBanUser?userID=testMan-unBan&adminUserID=VIPUser-unBan&enabled=false", null, (err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM sponsorTimes WHERE videoID = ? AND userID = ? AND shadowHidden = ?', ['unBan-videoID-0', 'testMan-unBan', 1]);
if (result.length !== 0) {
console.log(result);
done("Expected 0 banned entrys in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to unban a user and re-enable shadow banned segments without noSegment entrys', (done) => {
request.post(utils.getbaseURL() + "/api/shadowBanUser?userID=testWoman-unBan&adminUserID=VIPUser-unBan&enabled=false", null, (err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM sponsorTimes WHERE videoID = ? AND userID = ? AND shadowHidden = ?', ['unBan-videoID-1', 'testWoman-unBan', 1]);
if (result.length !== 1) {
console.log(result);
done("Expected 1 banned entry1 in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to unban a user and re-enable shadow banned segments with a mix of noSegment entrys', (done) => {
request.post(utils.getbaseURL() + "/api/shadowBanUser?userID=testEntity-unBan&adminUserID=VIPUser-unBan&enabled=false", null, (err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let result = db.prepare('all', 'SELECT * FROM sponsorTimes WHERE userID = ? AND shadowHidden = ?', ['testEntity-unBan', 1]);
if (result.length !== 1) {
console.log(result);
done("Expected 1 banned entry1 in db, got " + result.length);
} else {
done();
}
} else {
console.log(body);
done("Status code was " + res.statusCode);
}
});
});
});

View File

@@ -0,0 +1,371 @@
const request = require('request');
const config = require('../../src/config.js');
const { db, privateDB } = require('../../src/databases/databases.js');
const utils = require('../utils.js');
const getHash = require('../../src/utils/getHash.js');
describe('voteOnSponsorTime', () => {
before(() => {
const now = Date.now();
const warnVip01Hash = getHash("warn-vip01");
const warnUser01Hash = getHash("warn-voteuser01");
const warnUser02Hash = getHash("warn-voteuser02");
const MILLISECONDS_IN_HOUR = 3600000;
const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires;
let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES";
const startOfWarningQuery = 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES';
db.exec(startOfQuery + "('vote-testtesttest', 1, 11, 2, 'vote-uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest2', 1, 11, 2, 'vote-uuid-1', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest2', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest2', 1, 11, 10, 'vote-uuid-1.5', 'testman', 0, 50, 'outro', 0, '" + getHash('vote-testtesttest2', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest2', 1, 11, 10, 'vote-uuid-1.6', 'testman', 0, 50, 'interaction', 0, '" + getHash('vote-testtesttest2', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest3', 20, 33, 10, 'vote-uuid-2', 'testman', 0, 50, 'intro', 0, '" + getHash('vote-testtesttest3', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest,test', 1, 11, 100, 'vote-uuid-3', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest,test', 1) + "')");
db.exec(startOfQuery + "('vote-test3', 1, 11, 2, 'vote-uuid-4', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-test3', 1) + "')");
db.exec(startOfQuery + "('vote-test3', 7, 22, -3, 'vote-uuid-5', 'testman', 0, 50, 'intro', 0, '" + getHash('vote-test3', 1) + "')");
db.exec(startOfQuery + "('vote-multiple', 1, 11, 2, 'vote-uuid-6', 'testman', 0, 50, 'intro', 0, '" + getHash('vote-multiple', 1) + "')");
db.exec(startOfQuery + "('vote-multiple', 20, 33, 2, 'vote-uuid-7', 'testman', 0, 50, 'intro', 0, '" + getHash('vote-multiple', 1) + "')");
db.exec(startOfQuery + "('voter-submitter', 1, 11, 2, 'vote-uuid-8', '" + getHash("randomID") + "', 0, 50, 'sponsor', 0, '" + getHash('voter-submitter', 1) + "')");
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-9', '" + getHash("randomID2") + "', 0, 50, 'sponsor', 0, '" + getHash('voter-submitter2', 1) + "')");
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-10', '" + getHash("randomID3") + "', 0, 50, 'sponsor', 0, '" + getHash('voter-submitter2', 1) + "')");
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-11', '" + getHash("randomID4") + "', 0, 50, 'sponsor', 0, '" + getHash('voter-submitter2', 1) + "')");
db.exec(startOfQuery + "('own-submission-video', 1, 11, 500, 'own-submission-uuid', '"+ getHash('own-submission-id') +"', 0, 50, 'sponsor', 0, '" + getHash('own-submission-video', 1) + "')");
db.exec(startOfQuery + "('not-own-submission-video', 1, 11, 500, 'not-own-submission-uuid', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('not-own-submission-video', 1) + "')");
db.exec(startOfQuery + "('incorrect-category', 1, 11, 500, 'incorrect-category', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('incorrect-category', 1) + "')");
db.exec(startOfQuery + "('incorrect-category-change', 1, 11, 500, 'incorrect-category-change', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('incorrect-category-change', 1) + "')");
db.exec(startOfQuery + "('vote-testtesttest', 1, 11, 2, 'warnvote-uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest', 1) + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-1000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-2000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-3601000) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 1000)) + "', '" + warnVip01Hash + "')");
db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 2000)) + "', '" + warnVip01Hash + "')");
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("VIPUser") + "')");
privateDB.exec("INSERT INTO shadowBannedUsers (userID) VALUES ('" + getHash("randomID4") + "')");
});
it('Should be able to upvote a segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID&UUID=vote-uuid-0&type=1", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-0"]);
if (row.votes === 3) {
done()
} else {
done("Vote did not succeed. Submission went from 2 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to downvote a segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-2&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-2"]);
if (row.votes < 10) {
done()
} else {
done("Vote did not succeed. Submission went from 10 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to downvote the same segment when voting from a different user on the same IP', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID3&UUID=vote-uuid-2&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-2"]);
if (row.votes === 9) {
done()
} else {
done("Vote did not fail. Submission went from 9 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it("Should not be able to downvote a segment if the user is shadow banned", (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID4&UUID=vote-uuid-1.6&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-1.6"]);
if (row.votes === 10) {
done()
} else {
done("Vote did not fail. Submission went from 10 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it("Should not be able to upvote a segment if the user hasn't submitted yet", (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=hasNotSubmittedID&UUID=vote-uuid-1&type=1", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-1"]);
if (row.votes === 2) {
done()
} else {
done("Vote did not fail. Submission went from 2 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it("Should not be able to downvote a segment if the user hasn't submitted yet", (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=hasNotSubmittedID&UUID=vote-uuid-1.5&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-1.5"]);
if (row.votes === 10) {
done()
} else {
done("Vote did not fail. Submission went from 10 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('VIP should be able to completely downvote a segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=VIPUser&UUID=vote-uuid-3&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-3"]);
if (row.votes <= -2) {
done()
} else {
done("Vote did not succeed. Submission went from 100 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('should be able to completely downvote your own segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=own-submission-id&UUID=own-submission-uuid&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["own-submission-uuid"]);
if (row.votes <= -2) {
done()
} else {
done("Vote did not succeed. Submission went from 500 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('should not be able to completely downvote somebody elses segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=not-own-submission-uuid&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["not-own-submission-uuid"]);
if (row.votes === 499) {
done()
} else {
done("Vote did not succeed. Submission went from 500 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
/* Raised the limit for votes - test update needed
it('Should be able to vote for a category and it should immediately change (for now)', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-4&category=intro", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT category FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-4"]);
if (row.category === "intro") {
done()
} else {
done("Vote did not succeed. Submission went from sponsor to " + row.category);
}
} else {
done("Status code was " + res.statusCode);
}
});
});*/
it('Should not able to change to an invalid category', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=incorrect-category&category=fakecategory", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
let row = db.prepare('get', "SELECT category FROM sponsorTimes WHERE UUID = ?", ["incorrect-category"]);
if (row.category === "sponsor") {
done()
} else {
done("Vote did not succeed. Submission went from sponsor to " + row.category);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
/* Raised the limit for votes - test update needed
it('Should be able to change your vote for a category and it should immediately change (for now)', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-4&category=outro", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT category FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-4"]);
if (row.category === "outro") {
done()
} else {
done("Vote did not succeed. Submission went from intro to " + row.category);
}
} else {
done("Status code was " + res.statusCode);
}
});
});*/
it('Should not be able to change your vote to an invalid category', (done) => {
const vote = (inputCat, assertCat, callback) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=incorrect-category-change&category="+inputCat, null,
(err) => {
if (err) done(err);
else{
let row = db.prepare('get', "SELECT category FROM sponsorTimes WHERE UUID = ?", ["incorrect-category-change"]);
if (row.category === assertCat) {
callback();
} else {
done("Vote did not succeed. Submission went from sponsor to " + row.category);
}
}
});
};
vote("sponsor", "sponsor", () => {
vote("fakeCategory", "sponsor", done);
});
});
it('VIP should be able to vote for a category and it should immediately change', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=VIPUser&UUID=vote-uuid-5&category=outro", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT category FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-5"]);
let row2 = db.prepare('get', "SELECT votes FROM categoryVotes WHERE UUID = ? and category = ?", ["vote-uuid-5", "outro"]);
if (row.category === "outro" && row2.votes === 500) {
done()
} else {
done("Vote did not succeed. Submission went from intro to " + row.category + ". Category votes are " + row2.votes + " and should be 500.");
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to category-vote on an invalid UUID submission', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID3&UUID=invalid-uuid&category=intro", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 400) {
done();
} else {
done("Status code was " + res.statusCode + " instead of 400.");
}
});
});
it('Non-VIP should not be able to upvote "dead" submission', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-5&type=1", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done();
} else {
done("Status code was " + res.statusCode + " instead of 403");
}
});
});
it('VIP should be able to upvote "dead" submission', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=VIPUser&UUID=vote-uuid-5&type=1", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["vote-uuid-5"]);
if (row.votes > -3) {
done()
} else {
done("Vote did not succeed. Votes raised from -3 to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should not be able to upvote a segment (Too many warning)', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=warn-voteuser01&UUID=warnvote-uuid-0&type=1", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 403) {
done(); // success
} else {
done("Status code was " + res.statusCode);
}
});
});
});

51
test/mocks.js Normal file
View File

@@ -0,0 +1,51 @@
var express = require('express');
var app = express();
var config = require('../src/config.js');
app.post('/ReportChannelWebhook', (req, res) => {
res.sendStatus(200);
});
app.post('/FirstTimeSubmissionsWebhook', (req, res) => {
res.sendStatus(200);
});
app.post('/CompletelyIncorrectReportWebhook', (req, res) => {
res.sendStatus(200);
});
// Testing NeuralBlock
app.post('/NeuralBlockRejectWebhook', (req, res) => {
res.sendStatus(200);
});
app.get('/NeuralBlock/api/checkSponsorSegments', (req, res) => {
if (req.query.vid === "LevkAjUE6d4") {
res.json({
probabilities: [0.69]
});
return;
}
res.sendStatus(500);
});
//getSponsorSegments is no longer being used for automod
app.get('/NeuralBlock/api/getSponsorSegments', (req, res) => {
if (req.query.vid === "LevkAjUE6d4") {
res.json({
sponsorSegments: [[0.47,7.549],[264.023,317.293]]
});
return;
}
res.sendStatus(500);
});
// Testing webhooks
app.post('/CustomWebhook', (req, res) => {
res.sendStatus(200);
});
module.exports = function createMockServer(callback) {
return app.listen(config.mockPort, callback);
}

7
test/utils.js Normal file
View File

@@ -0,0 +1,7 @@
var config = require('../src/config.js');
module.exports = {
getbaseURL: () => {
return "http://localhost:" + config.port;
}
};

72
test/youtubeMock.js Normal file
View File

@@ -0,0 +1,72 @@
/*
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {});
*/
// https://developers.google.com/youtube/v3/docs/videos
const YouTubeAPI = {
listVideos: (id, callback) => {
YouTubeAPI.videos.list({
id: id
}, callback);
},
videos: {
list: (obj, callback) => {
if (obj.id === "knownWrongID") {
callback(undefined, {
pageInfo: {
totalResults: 0
},
items: []
});
} if (obj.id === "noDuration") {
callback(undefined, {
pageInfo: {
totalResults: 1
},
items: [
{
contentDetails: {
duration: "PT0S"
},
snippet: {
title: "Example Title",
thumbnails: {
maxres: {
url: "https://sponsor.ajay.app/LogoSponsorBlockSimple256px.png"
}
}
}
}
]
});
} else {
callback(undefined, {
pageInfo: {
totalResults: 1
},
items: [
{
contentDetails: {
duration: "PT1H23M30S"
},
snippet: {
title: "Example Title",
thumbnails: {
maxres: {
url: "https://sponsor.ajay.app/LogoSponsorBlockSimple256px.png"
}
}
}
}
]
});
}
}
}
};
module.exports = YouTubeAPI;