218 Commits

Author SHA1 Message Date
Alicia
178b5263d5 Update clan.go 2019-02-25 21:19:07 +00:00
Alicia
21d9cf3fae Update clan.go 2019-02-25 21:13:50 +00:00
Alicia
d7d592a2bd Update clan.go 2019-02-25 21:12:06 +00:00
Alicia
43bb674197 Update clan.go 2019-02-25 21:09:44 +00:00
Alicia
bd1b959836 adaq 2019-02-25 21:04:55 +00:00
Alicia
303b248408 Update clan.go 2019-02-25 20:57:06 +00:00
Alicia
1dabbe6b8c Update clan.go 2019-02-25 20:55:59 +00:00
Alicia
2e3c779330 Update clan.go 2019-02-24 14:56:50 +00:00
Alicia
3f08657c4d yea 2019-02-24 14:56:23 +00:00
Alicia
95754bffef czcz 2019-02-24 14:41:10 +00:00
Alicia
950340a04d add clan info to api 2019-02-24 14:37:51 +00:00
Alicia
e5b83bb11c Update clan.go 2019-02-24 14:29:19 +00:00
Alicia
2b02e0eaea Update user.go 2019-02-24 14:25:24 +00:00
Alicia
261c1549ed Update clan.go 2019-02-24 14:24:30 +00:00
Alicia
7b6bf4f562 add clans to api 2019-02-24 14:23:54 +00:00
Alicia
6f0df0a994 oops 2019-02-24 11:30:43 +00:00
Alicia
9f388f032c Forgot to update ranks 2019-02-24 11:26:46 +00:00
Alicia
53c06bd94f Update user.go 2019-02-24 00:38:51 +00:00
Alicia
a2916a6045 Update user.go 2019-02-24 00:37:52 +00:00
Alicia
13b6017e37 Update user.go 2019-02-24 00:36:31 +00:00
Alicia
47d0c75345 add rx and ap calls to api 2019-02-24 00:35:04 +00:00
Alicia
5f8eb862dd users.privileges 2019-02-23 14:22:28 +00:00
Alicia
df6cbafb00 dadadada 2019-02-23 14:21:31 +00:00
Alicia
43c63e57b3 ffafafa 2019-02-23 14:13:27 +00:00
Alicia
0c7b815edd crazy 2019-02-23 14:11:21 +00:00
Alicia
f6bbeec56e forgot to do these 2019-02-23 14:10:04 +00:00
Alicia
21646d8db7 Add relaxboard and autoboard to api 2019-02-23 14:06:24 +00:00
Alicia
b444bf15dd final error fixing 2019-02-23 13:14:37 +00:00
Alicia
5fc84bd914 fix schiavo error and changing rippleapi to api 2019-02-23 13:11:47 +00:00
Alicia
5b78c99b04 Allow import straight from github 2019-02-23 13:09:10 +00:00
Alicia
9c44c15e8a loL 2019-02-03 13:23:56 +00:00
Alicia
e1ed11d49f now we're cooking with gas 2019-02-03 13:21:31 +00:00
Alicia
69123c57a6 Didnt happen 2019-02-03 13:07:54 +00:00
Alicia
0de2f2afc0 Brian 2019-02-03 13:06:43 +00:00
Alicia
147274ebcb All 3 in 1 2019-02-03 12:59:47 +00:00
Alicia
333ca2743d Add rx and ap 2019-02-03 12:49:32 +00:00
qewc
2eda6c7aff user score ap 2019-02-03 12:45:37 +00:00
qewc
133b744bd2 Make users scores relax 2019-02-03 12:45:15 +00:00
qewc
a0c1da8c70 Add other playmode to api 2019-02-03 12:44:15 +00:00
Morgan Bazalgette
50ffa4e066 forbid empty querying scores 2018-07-19 15:58:35 +02:00
Morgan Bazalgette
daf10b5d71 memes 2018-07-19 15:52:19 +02:00
Morgan Bazalgette
21e3939b36 how do we not have querying by score id??? 2018-07-19 15:43:58 +02:00
Morgan Bazalgette
4470ca2e63 Add support for scores_removed 2018-06-21 22:18:48 +02:00
Morgan Bazalgette
334c409e9e Explain why you shouldn't design APIs like Ripple's in the README 2018-06-02 20:43:11 +02:00
Morgan Bazalgette
7e531b4ff4 Restrict/lock/ban/change privs via API 2018-05-20 18:57:27 +02:00
Morgan Bazalgette
e7a15cc9bc add POST /scores/reports 2018-05-03 22:28:38 +02:00
Morgan Bazalgette
5ed6601359 Remove 'Username change' note, because that is already handled by pep.py 2018-03-06 21:26:23 +01:00
Morgan Bazalgette
b35dd9aebf add user score wipe
i also forgot to add agplwarning in the vendor folder haha
2018-02-25 11:30:46 +01:00
Morgan Bazalgette
741b1e0b9f Require users to agree to license before starting the API 2018-02-04 11:54:35 +01:00
Morgan Bazalgette
77ba8aee78 lolol i forgot this 2018-01-23 22:45:59 +01:00
Morgan Bazalgette
a1eb970382 add API endpoint to retrieve user achievements 2018-01-21 21:21:46 +01:00
Morgan Bazalgette
39078d00a3 on /api/v1/scores, make users with UserManager not see scores by restricted users 2018-01-02 12:30:32 +01:00
Morgan Bazalgette
3130863456 custom rap logs 2017-12-09 09:36:00 +01:00
Morgan Bazalgette
982b9ab9f8 use coalesce rather than notes directly 2017-12-08 15:40:45 +01:00
Morgan Bazalgette
0cb01f6067 show email when token has manageusers 2017-11-21 15:53:23 +01:00
Morgan Bazalgette
8d92df534c make /users/full show CM notes and ban date when token has PrivilegeManageUser 2017-11-20 14:33:58 +01:00
Morgan Bazalgette
366418f025 log username/country changes 2017-11-18 16:49:49 +01:00
Morgan Bazalgette
c1e892336c create POST /api/v1/users/edit
now i need to make the docs for it kill me
2017-11-18 16:28:59 +01:00
Morgan Bazalgette
6dde2086ac We shall never forget about #ilbagdellennepì
add support for taiko pp
2017-09-18 21:38:33 +02:00
Morgan Bazalgette
77093ebef7 add Content-Type: json to peppy methods 2017-09-03 14:35:23 +02:00
Morgan Bazalgette
73f4a888c5 Allow IN for beatmap_id, beatmapset_id, beatmap_md5 2017-08-09 23:45:45 +02:00
Morgan Bazalgette
b7c00722de implement subscribe_mp_complete_match 2017-08-09 12:46:50 +02:00
Morgan Bazalgette
6925ce4c6e Remove deprecated API methods 2017-07-26 19:17:25 +02:00
Morgan Bazalgette
2535a03c5f Revert "Vendor update"
This reverts commit e5f062ee91.
2017-07-25 15:09:02 +02:00
Morgan Bazalgette
e5f062ee91 Vendor update 2017-07-25 14:58:07 +02:00
Morgan Bazalgette
8ebe5f6a02 Require client to specify explicitly in websockets whether restricted users should be seen
This is only allowed to those having the user privilege AdminPrivilegeManageUsers, having being identified by the API AND having sent a message of type set_restricted_visibility stating specifically in the data that they want to get info also about restricted users.
This also includes some more information in the new_scores, such as the username and userid of the user who submitted the score.
2017-07-25 14:49:14 +02:00
Morgan Bazalgette
60d48df46d Merge branch 'oauth2' 2017-07-25 10:41:09 +02:00
Morgan Bazalgette
a409231ca0 Some whitespace fixes 2017-07-25 10:39:37 +02:00
Morgan Bazalgette
6352f752ca Add AdminPrivilegeCaker 2017-07-23 22:38:27 +02:00
Morgan Bazalgette
c078e20ac5 Pagination and filer by ID for GET /tokens 2017-07-01 21:37:25 +02:00
Morgan Bazalgette
9be90df7bd ping/pong 2017-06-27 00:33:21 +02:00
Morgan Bazalgette
1136738111 Implement bearer tokens 2017-06-17 18:11:10 +02:00
Morgan Bazalgette
155750b746 New quotes, featuring: crostata, thermodynamics, Rick and Snowball 2017-06-13 10:03:08 +02:00
Morgan Bazalgette
e766e951a5 Remove documentation endpoints
The documentation system is getting changed, https://github.com/osuripple/website-docs is going to be used for documentation instead of the database.
2017-05-01 18:22:18 +02:00
Morgan Bazalgette
8a3dae0592 Add endpoint to retrieve people who have a certain badge 2017-05-01 18:05:22 +02:00
Morgan Bazalgette
ace4c6bee0 make sure panics don't slip out of websockets 2017-04-18 21:13:31 +02:00
Morgan Bazalgette
330b90b172 Add on-the-fly rank calculation; change "subscribed" to "subscribed_to_scores" 2017-04-18 21:08:06 +02:00
Morgan Bazalgette
b2c3ada7c0 add back datadog "requests" 2017-04-13 09:23:46 +02:00
Morgan Bazalgette
cc90105130 make Global- and CountryLeaderboardRank nullable 2017-04-13 08:57:11 +02:00
Morgan Bazalgette
622658f5aa Implement country leaderboards 2017-04-11 23:18:02 +02:00
Morgan Bazalgette
120c027fce Fix 500 on user scores 2017-02-19 19:01:13 +01:00
Morgan Bazalgette
69678f21f5 change some emsm 2017-02-19 18:40:15 +01:00
Morgan Bazalgette
6f9cae0bcd Add initial websocket implementation 2017-02-19 18:19:59 +01:00
Morgan Bazalgette
9b296fc8ed Medium changed API's Snippet to Subtitle 2017-02-17 21:18:41 +01:00
Morgan Bazalgette
0315dd61d2 Fix author in posts not working, remove relative times 2017-02-16 22:05:55 +01:00
Morgan Bazalgette
bc2100b889 Change Medium API endpoint 2017-02-16 21:47:47 +01:00
Morgan Bazalgette
021e76cab7 Update vendor (remove gin) 2017-02-02 15:16:01 +01:00
Morgan Bazalgette
490d13e333 Add back old middlewares 2017-02-02 15:13:17 +01:00
Morgan Bazalgette
736c904f65 Update vendor 2017-02-02 13:53:44 +01:00
Morgan Bazalgette
68a9808942 Wrap fasthttprouter inside own router 2017-02-02 13:53:36 +01:00
Morgan Bazalgette
85e6dc7e5e Move to fasthttp for improved performance 2017-02-02 13:40:28 +01:00
Morgan Bazalgette
ace2fded7e Up the API rate limits slightly 2017-02-02 11:56:32 +01:00
Morgan Bazalgette
0d58fd3f63 add result limiting in blog posts 2017-01-28 15:41:12 +01:00
Morgan Bazalgette
ab89bda819 Make Blog API use Medium 2017-01-28 15:06:12 +01:00
Morgan Bazalgette
3961e310b1 vendor! 2017-01-14 18:42:10 +01:00
Morgan Bazalgette
41ee4c90b3 Move from git.zxq.co to zxq.co 2017-01-14 18:06:16 +01:00
Morgan Bazalgette
62612cfdb8 Allow for searching user by email for users with ManageUser 2016-12-12 21:35:18 +01:00
Morgan Bazalgette
e4af28b8ff Add flags to UsersSelfSettingsGET 2016-12-11 18:38:36 +01:00
Morgan Bazalgette
099983f7f3 Add flags (EmailVerified, Country2FA) 2016-12-11 16:40:07 +01:00
Howl
256f082340 update last_updated when calling the API with a token 2016-12-01 23:16:36 +01:00
Howl
dd536eebed Add link to mirror 2016-11-29 16:18:41 +01:00
Howl
5fd38f4595 Fix AKA usernames that cannot be disabled 2016-11-23 23:09:06 +01:00
Howl
b0c4eb24e5 add icon sanitisation 2016-11-23 20:19:11 +01:00
Howl
8dc5af9406 Rename quite a few methods 2016-11-21 17:04:27 +01:00
Howl
700170392a [API CHANGE] /tokens/self/delete now requires post 2016-11-21 17:02:25 +01:00
Howl
20dba6cd86 Input sanitisation in userpages and user settings 2016-11-21 16:59:17 +01:00
Howl
78a1c1d038 Fix some pagination memes 2016-11-21 16:27:21 +01:00
Howl
7a65b705d6 Not Enough UTF8MB4 2016-11-20 13:46:44 +01:00
Howl
055a4cdc10 Use JSON instead of GLI ZINGONI 2016-11-20 12:06:11 +01:00
Howl
aef33f708a ripple: -> lets: 2016-11-20 11:40:01 +01:00
Howl
ac2deb9ae0 Return rank request status after requesting a beatmap 2016-11-19 20:46:52 +01:00
Howl
26435c1195 Add beatmap rank requests 2016-11-19 19:53:55 +01:00
Howl
9e57fedd80 friends/add and del are now POST-only 2016-11-16 18:03:47 +01:00
Howl
4036772803 Userpage now is never nil (fix edit userpage not working for certain users) 2016-11-16 17:36:01 +01:00
Howl
9df1fd9e97 Add back r.Email 2016-11-13 19:20:07 +01:00
Howl
6d3d6cde2d Don't allow email to be modified from user settings 2016-11-12 18:51:50 +01:00
Howl
1c2201317f Add POST users/self/userpage 2016-11-07 18:34:53 +01:00
Howl
6c8d7baee2 add UsersSelfSettingsGET and POST 2016-11-06 22:51:21 +01:00
Howl
9de913888d Light speedup on scores/recent 2016-10-29 17:07:29 +02:00
Howl
b9d4683092 Remove time from sorting in score 2016-10-29 14:27:55 +02:00
Howl
10c61cec02 beatmap_md5 -> md5 2016-10-29 14:23:31 +02:00
Howl
a6b4ad1d3e Allow looking for beatmap md5 in GET /beatmaps 2016-10-29 14:05:16 +02:00
Howl
3ce1f58a48 add pagination to GET /tokens 2016-10-28 16:45:30 +02:00
Howl
4239c2f0e9 Add mods to get_scores 2016-10-25 20:03:33 +02:00
Howl
077c6563fa StatusLoved 2016-10-25 18:48:44 +02:00
Howl
29296010db Add silence info 2016-10-21 18:54:46 +02:00
Howl
2f33e94881 add has_not_privileges 2016-10-20 18:14:30 +02:00
Howl
043448c429 Add search by privilege group name in users 2016-10-19 17:10:36 +02:00
Howl
325df61d96 Spaces and underscores in usernames now don't make a difference 2016-10-16 18:52:34 +02:00
Howl
2e1713db49 add SafeUsername function 2016-10-16 18:27:12 +02:00
Howl
0edbff13cd Add sorting stuff to GET v1/friends 2016-10-16 16:45:42 +02:00
Howl
fefde77261 GET v1/users is now actually pretty darn cool 2016-10-16 16:26:10 +02:00
Howl
dccec469af Add UserTournamentStaff privilege 2016-10-06 16:57:01 +02:00
Howl
e5504d2e29 whoops 2016-10-02 22:30:52 +02:00
Howl
2e2b1e481b Revert "requests -> requestsbytype"
This reverts commit a060187547.
2016-10-02 21:59:04 +02:00
Howl
46c723c7b6 I cannot believe I am so stupid 2016-10-02 21:58:43 +02:00
Howl
a060187547 requests -> requestsbytype 2016-10-02 21:54:23 +02:00
Howl
3a55524c86 Add some more datadog crap 2016-10-02 20:07:38 +02:00
Howl
da0021a9b3 Add some datadog shit 2016-10-02 19:47:31 +02:00
Howl
c54c1565ca Show custom badge only if user is currently a donor 2016-09-30 21:11:45 +02:00
Howl
48cef26ccd Add ability to filter leaderboard by country (country ranking) (NOT ON WEBSITE RN) 2016-09-28 23:00:24 +02:00
Howl
d6aa0a814d Level on chosen_mode in leaderboard 2016-09-28 20:49:58 +02:00
Howl
90b7d71062 Increase max users on leaderboard limit to 500 2016-09-28 18:43:56 +02:00
Howl
017421b232 Add users/self/favourite_mode 2016-09-27 23:13:55 +02:00
Howl
b0ffad5e76 Add title to doc/rules 2016-09-26 20:07:47 +02:00
Howl
a197647933 Include doc title in documentation content get 2016-09-26 17:16:56 +02:00
Howl
0e27793d25 make rank_requests/status auth-free 2016-09-24 19:45:07 +02:00
Howl
3262bbea74 rank requests stuff, temporarily disabled 2016-09-20 18:14:02 +02:00
Howl
42ff99bcc7 Update gitignore to include vscode's trash 2016-09-17 23:27:36 +02:00
Howl
e858ed94bc Schiavo is like a son with a down syndrome.
You love him, like every child you have, but you have to accept the fact that he's fucking down, and so he's completely retarded and makes everything the wrong way.
Also, in this commit: set_status is now no more fucking retarded, as in it actually returns the beatmap it was requested, and some minor things.
2016-09-17 23:27:20 +02:00
Howl
4be5948d97 Fix get_users returning no results if user doesn't have a leaderboard position 2016-09-09 18:55:26 +02:00
Howl
7249d9136b move rank requests status to ReadConfidential, as it contains user information 2016-09-07 12:53:52 +02:00
Howl
b81dffcecc add bgeatmapget 2016-09-07 01:51:23 +02:00
Howl
dd00c39075 Add initial beatmap reqeusts API to get current status 2016-09-06 20:11:46 +02:00
Howl
6eda6aea87 Add == nil to limit's check() 2016-09-06 19:17:18 +02:00
Howl
c745947281 add peppymethods also in /v1, because new osu-web does so 2016-09-06 19:15:37 +02:00
Howl
1826246125 Create `limit' package, remove need of login_attempts.go 2016-09-06 17:04:22 +02:00
Howl
7e5d35d79a make userpage content nil-able 2016-09-05 23:45:11 +02:00
Howl
9bfbb50ffd ping now returns both user and token privileges and their string representation 2016-09-03 01:31:03 +02:00
Howl
61527882d1 Get donor info through /users/self/donor_info 2016-09-02 18:27:15 +02:00
Howl
92be078783 move peppyapi into its own group 2016-09-02 18:14:23 +02:00
Howl
e80e557f10 Add custom badge in users/full 2016-09-02 17:00:36 +02:00
Howl
e6c77e64a8 Pagination in badgesGET 2016-09-01 22:58:21 +02:00
Howl
12aaa04fea use new badge system, users/full now returns badge information rather than just the ID 2016-09-01 22:50:56 +02:00
Howl
76c653b47e Fucking typos 2016-08-27 12:52:17 +02:00
Howl
e4d27f8d6b Allow users with AdminManageUsers to see banned users
Also:
- General code refactoring
- Allow banned/restricted users to see their scores etc
- common.MethodData now contains UserPrivileges
- UserPrivileges have now their own type
- Implement md.HasQuery, to know if there's a GET querystring parameter or not
2016-08-27 12:04:12 +02:00
Howl
476cd385f8 tiny get_beatmaps performance improvement 2016-08-23 21:30:11 +02:00
Howl
4d9f466491 Forgot a break inf a for-range loop. Broke all the mode detection system. 2016-08-23 16:45:47 +02:00
Howl
3de3443d87 this is driving me crazy 2016-08-23 16:39:57 +02:00
Howl
42011ad10c Allow much more freedom to query for different things in /api/v1/beatmaps 2016-08-23 16:27:09 +02:00
Howl
f35bb0a7e8 Fix m = 0 killing the query 2016-08-23 15:42:59 +02:00
Howl
73b0f48586 Add get_beatmaps 2016-08-22 23:04:52 +02:00
Howl
69e1d585cc LEFT to INNER 2016-08-19 23:48:44 +02:00
Howl
817592e736 Remove common.OsuTime, change all occurencies to common.UnixTimestamp 2016-08-19 17:02:51 +02:00
Howl
67ae6ab3b8 Fix users/full returning "" as the country 2016-08-18 15:15:22 +02:00
Howl
55d7621ef5 remove -o api, as it's the evil 2016-08-15 21:56:29 +02:00
Howl
4f7ef9c071 u param 2016-08-15 21:51:51 +02:00
Howl
cab63a94ac Make rank actually contain rank 2016-08-15 21:37:24 +02:00
Howl
954ec755b8 Implement get_scores in peppyapi 2016-08-15 21:13:40 +02:00
Howl
b9d35ac46d Add more where clauses because I forgot 'em 2016-08-15 20:07:40 +02:00
Howl
a6ca8de13e Implement GET scores in official ripple api 2016-08-15 19:59:46 +02:00
Howl
346f26177c Change all references to sql.DB to references to sqlx.DB 2016-08-15 13:45:42 +02:00
Howl
e41be44397 Use sqlx instead of sql. Rewrite of most clusterfucks incoming 2016-08-15 13:37:03 +02:00
Howl
c3b56164f5 Change X-Ripple-Token cookie to simply "rt" 2016-08-10 12:25:26 +02:00
Howl
f1fa5e8b91 Allow to get a blog post's content by slug 2016-08-10 09:45:01 +02:00
Howl
af691bee1c Change system for hanayo requests 2016-08-09 23:06:23 +02:00
Howl
4f217781cc GET /tokens/self now returns 404 if no token is given 2016-08-09 19:22:41 +02:00
Howl
17fef6c654 Silently ignore time.ParseErrors 2016-08-09 19:15:53 +02:00
Howl
a9fb74984b Change README 2016-08-09 19:01:54 +02:00
Howl
2b3ac412ad Proprietary -> AGPL 2016-08-09 18:58:30 +02:00
Nyo
38008a5451 Fix error on scores handlers 2016-07-20 10:49:29 +02:00
Howl
3a2928be53 hey nyo i may or may not have fixed beatmap difficulties 2016-07-18 23:27:37 +02:00
Howl
a999033e4b Add X-Real-404 to true 404 responses 2016-07-12 19:55:58 +02:00
Howl
8544c41cc6 Requests from hanayo have no rate limiting 2016-07-12 15:49:02 +02:00
Howl
6926cda1ee Fix banned response code, fix typo 2016-07-10 20:53:06 +02:00
Howl
ab8e1ad7e2 Log errors 2016-07-07 17:36:53 +02:00
Howl
921402b0ee Add blog API 2016-07-07 00:20:36 +02:00
Howl
24933cc08f Show countries of everyone, without checking show_country. 2016-07-06 23:43:43 +02:00
Howl
09523369b7 Use custom type UnixTimestamp instead of repeating boilerplate code over and over. 2016-07-06 22:32:30 +02:00
Howl
ff1d2fa1c3 .HIDE. shit 2016-07-06 19:36:46 +02:00
Howl
15559d0be2 use c.ClientIP() rather than manually accessing c.Request.RemoteAddr 2016-07-06 19:35:49 +02:00
Howl
45d0de234b Fix race condition in specificRateLimiter 2016-07-06 19:24:11 +02:00
Howl
faf948b037 Implement rate limiting
- 60 requests per minute for requests without a valid API token
- 2000 requests per minute per user for requests with a valid API token
2016-07-06 16:33:58 +02:00
Howl
0a870ee742 Remove read privilege. Public data is now readable by everyone without having to pass an API token. Feel free to test around as much as you like! 2016-07-06 14:22:43 +02:00
Howl
534d5183ed It's not rank, it's privileges 2016-07-06 14:12:57 +02:00
Howl
fcdd042d6c Finish up with new ranks and stuff on the API 2016-07-04 00:06:23 +02:00
Nyo
39f6b2bbcf New privileges on API (allowed column only) 2016-07-03 21:55:03 +02:00
Nyo
244186cf4e Allow compilation of the API on Windows 2016-06-22 13:13:33 +02:00
Howl
7cbbb626c1 LEFT JOIN -> INNER JOIN 2016-06-18 02:42:47 +02:00
Howl
b3e31734f5 Release logging to sentry 2016-06-17 10:34:53 +02:00
Howl
427f2ff890 Remove errore_meme 2016-06-16 14:06:09 +02:00
Howl
02f0449b39 Stack traces are only useful for panics 2016-06-16 14:05:26 +02:00
Howl
6554447cbc Use ID rather than IP, memes 2016-06-16 14:01:37 +02:00
Howl
ef9c0c6c6a When erroring from c.Error(), give actual error, not a *gin.Error 2016-06-16 13:59:18 +02:00
Howl
9658957067 Better recovery 2016-06-16 13:49:35 +02:00
Howl
cbcfacb06a Fix misplaced if 2016-06-16 13:14:19 +02:00
Howl
06eb64ecf5 error logging with schiavo -> error logging with sentry 2016-06-16 12:45:10 +02:00
Howl
24d34eb741 v1.TokenNewPOST => /tokens, /tokens/new 2016-06-15 00:23:55 +02:00
326 changed files with 62874 additions and 1238 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
rippleapi rippleapi
rippleapi.exe
api api
api.conf api.conf
debug
launch.json

622
LICENSE
View File

@@ -1,3 +1,619 @@
Copyright (C) The Ripple Developers - All Rights Reserved GNU AFFERO GENERAL PUBLIC LICENSE
Unauthorized copying of this file, via any medium is strictly prohibited Version 3, 19 November 2007
Proprietary and confidential
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS

View File

@@ -1,9 +1,20 @@
# osuripple/api # rippleapi
This is the source code for the Ripple API. It's still in its very early development, although I plan on putting it in an early beta phase in about a couple of weeks. This is the source code for Ripple's API.
[Public roadmap (it's actually just a trello board)](https://trello.com/b/oL43fuPa/api) - Origin: https://git.github.com/osuyozora/api
- Mirror: https://github.com/osuyozora/api
## License ## Note to fellow developers: this is not how you do it!
The API is crammed with terrible design. First of all, it is not RESTful, and as you'll come to learn, designing an API in a RESTful manner is good because it helps to create consistent design (across your API and other APIs). It also quite simplifies many other things:
* In the API, to get a specific item, you need to do e.g. `/users?id=1009`. It's much more useful to have these in the URL path directly (`/users/1009`) for a number of reasons:
* It simplifies checks (`/users/scores?id=1009` will require a check to see if an ID is present. `/users/:id/scores` doesn't really need a check, because `/users/scores` won't match)
* It gives a "feel" of hierarchy
* There is no multiple ways of addressing a specific user. There is a single way: IDs. In the Ripple API, you can specify an username instead of an ID to get a specific user, but this is prone to failure in the event of the user changing the username, whereas an ID cannot (should not) change.
* You can show error codes to the user using HTTP status codes. This way, you can present the resource to the user without any wrapper (such as an object giving an "ok" field or, like in the API, a `code` parameter), so the user can likely reuse other parts for error handling that they already use for other http requests.
* GET merely shows a resource, is cacheable and "idempotent". This helps debugging (repeating the same request twice will yield the same result, unless of course the data changes otherwise), caching (you can answer with Cache-Control headers, which browsers understand).
The not-making-it-RESTful was the biggest sin of the API. In itself, the API was a step into the right direction (it is MUCH better than the official osu! API), but nowhere close to how an API actually is, ideally. If you are building an API, I won't recommend you a book, but instead I will recommend you to see what [GitHub](https://developer.github.com/v3/) does, as they will have probably faced most problems that you have, and provided an answer already. If you're unsure, check other APIs: Discord, Slack, Twitter, Stripe, to name a few.
MIT

View File

@@ -1,27 +0,0 @@
package app
import (
"fmt"
"git.zxq.co/ripple/schiavolib"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
)
// ErrorHandler is a middleware for gin that takes care of calls to c.Error().
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
errs := c.Errors.Errors()
if len(errs) != 0 {
color.Red("!!! ERRORS OCCURRED !!!")
var out string
out += fmt.Sprintf("==> %s %s\n", c.Request.Method, c.Request.URL.Path)
for _, err := range errs {
out += fmt.Sprintf("===> %s\n", err)
}
color.Red(out)
go schiavo.Bunker.Send("Errors occurred:\n```\n" + out + "```")
}
}
}

View File

@@ -1,17 +1,11 @@
// Package internals has methods that suit none of the API packages. // Package internals has methods that suit none of the API packages.
package internals package internals
import ( import "github.com/valyala/fasthttp"
"github.com/gin-gonic/gin"
)
type statusResponse struct { var statusResp = []byte(`{ "status": 1 }`)
Status int `json:"status"`
}
// Status is used for checking the API is up by the ripple website, on the status page. // Status is used for checking the API is up by the ripple website, on the status page.
func Status(c *gin.Context) { func Status(c *fasthttp.RequestCtx) {
c.JSON(200, statusResponse{ c.Write(statusResp)
Status: 1,
})
} }

View File

@@ -2,83 +2,95 @@ package app
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "fmt"
"reflect"
"regexp" "regexp"
"strings"
"unsafe"
"git.zxq.co/ripple/rippleapi/common" "github.com/valyala/fasthttp"
"github.com/gin-gonic/gin" "github.com/osuyozora/api/common"
) )
// Method wraps an API method to a HandlerFunc. // Method wraps an API method to a HandlerFunc.
func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) gin.HandlerFunc { func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) fasthttp.RequestHandler {
return func(c *gin.Context) { return func(c *fasthttp.RequestCtx) {
initialCaretaker(c, f, privilegesNeeded...) initialCaretaker(c, f, privilegesNeeded...)
} }
} }
func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) { func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
data, err := ioutil.ReadAll(c.Request.Body) var doggoTags []string
if err != nil {
c.Error(err)
}
c.Request.Body.Close()
token := "" qa := c.Request.URI().QueryArgs()
var token string
var bearerToken bool
switch { switch {
case c.Request.Header.Get("X-Ripple-Token") != "": case len(c.Request.Header.Peek("X-Ripple-Token")) > 0:
token = c.Request.Header.Get("X-Ripple-Token") token = string(c.Request.Header.Peek("X-Ripple-Token"))
case c.Query("token") != "": case strings.HasPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer "):
token = c.Query("token") token = strings.TrimPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer ")
case c.Query("k") != "": bearerToken = true
token = c.Query("k") case len(qa.Peek("token")) > 0:
token = string(qa.Peek("token"))
case len(qa.Peek("k")) > 0:
token = string(qa.Peek("k"))
default: default:
token, _ = c.Cookie("X-Ripple-Token") token = string(c.Request.Header.Cookie("rt"))
} }
md := common.MethodData{ md := common.MethodData{
DB: db, DB: db,
RequestData: data, Ctx: c,
C: c, Doggo: doggo,
R: red,
} }
if token != "" { if token != "" {
tokenReal, exists := GetTokenFull(token, db) var (
tokenReal common.Token
exists bool
)
if bearerToken {
tokenReal, exists = BearerToken(token, db)
} else {
tokenReal, exists = GetTokenFull(token, db)
}
if exists { if exists {
md.User = tokenReal md.User = tokenReal
doggoTags = append(doggoTags, "authorised")
} }
} }
// log into datadog that this is an hanayo request
if b2s(c.Request.Header.Peek("H-Key")) == cf.HanayoKey && b2s(c.UserAgent()) == "hanayo" {
doggoTags = append(doggoTags, "hanayo")
}
doggo.Incr("requests.v1", doggoTags, 1)
missingPrivileges := 0 missingPrivileges := 0
for _, privilege := range privilegesNeeded { for _, privilege := range privilegesNeeded {
if int(md.User.Privileges)&privilege == 0 { if uint64(md.User.TokenPrivileges)&uint64(privilege) == 0 {
missingPrivileges |= privilege missingPrivileges |= privilege
} }
} }
if missingPrivileges != 0 { if missingPrivileges != 0 {
c.IndentedJSON(401, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+".")) c.SetStatusCode(401)
mkjson(c, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
return return
} }
resp := f(md) resp := f(md)
if resp.GetCode() == 0 { if md.HasQuery("pls200") {
// Dirty hack to set the code c.SetStatusCode(200)
type setCoder interface { } else {
SetCode(int) c.SetStatusCode(resp.GetCode())
}
if newver, can := resp.(setCoder); can {
newver.SetCode(500)
}
} }
if _, exists := c.GetQuery("pls200"); exists { if md.HasQuery("callback") {
c.Writer.WriteHeader(200) c.Response.Header.Add("Content-Type", "application/javascript; charset=utf-8")
} else { } else {
c.Writer.WriteHeader(resp.GetCode()) c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
}
if _, exists := c.GetQuery("callback"); exists {
c.Header("Content-Type", "application/javascript; charset=utf-8")
} else {
c.Header("Content-Type", "application/json; charset=utf-8")
} }
mkjson(c, resp) mkjson(c, resp)
@@ -88,22 +100,45 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe
var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`) var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
// mkjson auto indents json, and wraps json into a jsonp callback if specified by the request. // mkjson auto indents json, and wraps json into a jsonp callback if specified by the request.
// then writes to the gin.Context the data. // then writes to the RequestCtx the data.
func mkjson(c *gin.Context, data interface{}) { func mkjson(c *fasthttp.RequestCtx, data interface{}) {
exported, err := json.MarshalIndent(data, "", "\t") exported, err := json.MarshalIndent(data, "", "\t")
if err != nil { if err != nil {
c.Error(err) fmt.Println(err)
exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong.", "data": null }`) exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong." }`)
} }
cb := c.Query("callback") cb := string(c.URI().QueryArgs().Peek("callback"))
willcb := cb != "" && willcb := cb != "" &&
len(cb) < 100 && len(cb) < 100 &&
callbackJSONP.MatchString(cb) callbackJSONP.MatchString(cb)
if willcb { if willcb {
c.Writer.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "(")) c.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
} }
c.Writer.Write(exported) c.Write(exported)
if willcb { if willcb {
c.Writer.Write([]byte(");")) c.Write([]byte(");"))
} }
} }
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
//
// Note it may break if string and/or slice header will change
// in the future go versions.
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// s2b converts string to a byte slice without memory allocation.
//
// Note it may break if string and/or slice header will change
// in the future go versions.
func s2b(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}

View File

@@ -1,38 +1,133 @@
package peppy package peppy
import ( import (
"database/sql" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/jmoiron/sqlx"
"github.com/thehowl/go-osuapi"
"github.com/valyala/fasthttp"
"github.com/osuyozora/api/common"
) )
// GetBeatmap retrieves general beatmap information. // GetBeatmap retrieves general beatmap information.
func GetBeatmap(c *gin.Context, db *sql.DB) { func GetBeatmap(c *fasthttp.RequestCtx, db *sqlx.DB) {
var whereClauses []string var whereClauses []string
var params []string var params []interface{}
limit := strconv.Itoa(common.InString(1, query(c, "limit"), 500, 500))
// since value is not stored, silently ignore // since value is not stored, silently ignore
if c.Query("s") != "" { if query(c, "s") != "" {
whereClauses = append(whereClauses, "beatmaps.beatmapset_id = ?") whereClauses = append(whereClauses, "beatmaps.beatmapset_id = ?")
params = append(params, c.Query("s")) params = append(params, query(c, "s"))
} }
if c.Query("b") != "" { if query(c, "b") != "" {
whereClauses = append(whereClauses, "beatmaps.beatmap_id = ?") whereClauses = append(whereClauses, "beatmaps.beatmap_id = ?")
params = append(params, c.Query("b")) params = append(params, query(c, "b"))
// b is unique, so change limit to 1
limit = "1"
} }
if c.Query("u") != "" { // creator is not stored, silently ignore u and type
wc, p := genUser(c, db) if query(c, "m") != "" {
whereClauses = append(whereClauses, wc) m := genmode(query(c, "m"))
params = append(params, p) if m == "std" {
// Since STD beatmaps are converted, all of the diffs must be != 0
for _, i := range modes {
whereClauses = append(whereClauses, "beatmaps.difficulty_"+i+" != 0")
} }
// silently ignore m } else {
// silently ignore a whereClauses = append(whereClauses, "beatmaps.difficulty_"+m+" != 0")
if c.Query("h") != "" { if query(c, "a") == "1" {
whereClauses = append(whereClauses, "beatmaps.difficulty_std = 0")
}
}
}
if query(c, "h") != "" {
whereClauses = append(whereClauses, "beatmaps.beatmap_md5 = ?") whereClauses = append(whereClauses, "beatmaps.beatmap_md5 = ?")
params = append(params, c.Query("h")) params = append(params, query(c, "h"))
} }
//bm := osuapi.Beatmap{} where := strings.Join(whereClauses, " AND ")
if where != "" {
//db.Query("SELECT beatmaps.beatmapset_id, beatmaps.beatmap FROM ") where = "WHERE " + where
}
rows, err := db.Query(`SELECT
beatmapset_id, beatmap_id, ranked, hit_length,
song_name, beatmap_md5, ar, od, bpm, playcount,
passcount, max_combo, difficulty_std, difficulty_taiko, difficulty_ctb, difficulty_mania,
latest_update
FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
params...)
if err != nil {
common.Err(c, err)
json(c, 200, defaultResponse)
return
}
var bms []osuapi.Beatmap
for rows.Next() {
var (
bm osuapi.Beatmap
rawRankedStatus int
rawName string
rawLastUpdate common.UnixTimestamp
diffs [4]float64
)
err := rows.Scan(
&bm.BeatmapSetID, &bm.BeatmapID, &rawRankedStatus, &bm.HitLength,
&rawName, &bm.FileMD5, &bm.ApproachRate, &bm.OverallDifficulty, &bm.BPM, &bm.Playcount,
&bm.Passcount, &bm.MaxCombo, &diffs[0], &diffs[1], &diffs[2], &diffs[3],
&rawLastUpdate,
)
if err != nil {
common.Err(c, err)
continue
}
bm.TotalLength = bm.HitLength
bm.LastUpdate = osuapi.MySQLDate(rawLastUpdate)
if rawRankedStatus >= 2 {
bm.ApprovedDate = osuapi.MySQLDate(rawLastUpdate)
}
// zero value of ApprovedStatus == osuapi.StatusPending, so /shrug
bm.Approved = rippleToOsuRankedStatus[rawRankedStatus]
bm.Artist, bm.Title, bm.DiffName = parseDiffName(rawName)
for i, diffVal := range diffs {
if diffVal != 0 {
bm.Mode = osuapi.Mode(i)
bm.DifficultyRating = diffVal
break
}
}
bms = append(bms, bm)
}
json(c, 200, bms)
}
var rippleToOsuRankedStatus = map[int]osuapi.ApprovedStatus{
0: osuapi.StatusPending,
1: osuapi.StatusWIP, // it means "needs updating", as the one in the db needs to be updated, but whatever
2: osuapi.StatusRanked,
3: osuapi.StatusApproved,
4: osuapi.StatusQualified,
5: osuapi.StatusLoved,
}
// buggy diffname parser
func parseDiffName(name string) (author string, title string, diffName string) {
parts := strings.SplitN(name, " - ", 2)
author = parts[0]
if len(parts) > 1 {
title = parts[1]
if s := strings.Index(title, " ["); s != -1 {
diffName = title[s+2:]
if len(diffName) != 0 && diffName[len(diffName)-1] == ']' {
diffName = diffName[:len(diffName)-1]
}
title = title[:s]
}
}
return
} }

View File

@@ -2,27 +2,21 @@ package peppy
import ( import (
"database/sql" "database/sql"
_json "encoding/json"
"strconv" "strconv"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"github.com/gin-gonic/gin" "github.com/osuyozora/api/common"
) )
var modes = []string{"std", "taiko", "ctb", "mania"}
var defaultResponse = []struct{}{} var defaultResponse = []struct{}{}
func genmode(m string) string { func genmode(m string) string {
switch m { i := genmodei(m)
case "1": return modes[i]
m = "taiko"
case "2":
m = "ctb"
case "3":
m = "mania"
default:
m = "std"
}
return m
} }
func genmodei(m string) int { func genmodei(m string) int {
v := common.Int(m) v := common.Int(m)
@@ -31,34 +25,51 @@ func genmodei(m string) int {
} }
return v return v
} }
func rankable(m string) bool {
x := genmodei(m)
return x != 2
}
func genUser(c *gin.Context, db *sql.DB) (string, string) { func genUser(c *fasthttp.RequestCtx, db *sqlx.DB) (string, string) {
var whereClause string var whereClause string
var p string var p string
// used in second case of switch // used in second case of switch
s, err := strconv.Atoi(c.Query("u")) s, err := strconv.Atoi(query(c, "u"))
switch { switch {
// We know for sure that it's an username. // We know for sure that it's an username.
case c.Query("type") == "string": case query(c, "type") == "string":
whereClause = "users.username = ?" whereClause = "users.username_safe = ?"
p = c.Query("u") p = common.SafeUsername(query(c, "u"))
// It could be an user ID, so we look for an user with that username first. // It could be an user ID, so we look for an user with that username first.
case err == nil: case err == nil:
err = db.QueryRow("SELECT id FROM users WHERE id = ? LIMIT 1", s).Scan(&p) err = db.QueryRow("SELECT id FROM users WHERE id = ? LIMIT 1", s).Scan(&p)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// If no user with that userID were found, assume username. // If no user with that userID were found, assume username.
p = c.Query("u") whereClause = "users.username_safe = ?"
whereClause = "users.username = ?" p = common.SafeUsername(query(c, "u"))
} else { } else {
// An user with that userID was found. Thus it's an userID. // An user with that userID was found. Thus it's an userID.
whereClause = "users.id = ?" whereClause = "users.id = ?"
} }
// u contains letters, so it's an username. // u contains letters, so it's an username.
default: default:
p = c.Query("u") whereClause = "users.username_safe = ?"
whereClause = "users.username = ?" p = common.SafeUsername(query(c, "u"))
} }
return whereClause, p return whereClause, p
} }
func query(c *fasthttp.RequestCtx, s string) string {
return string(c.QueryArgs().Peek(s))
}
func json(c *fasthttp.RequestCtx, code int, data interface{}) {
c.SetStatusCode(code)
d, err := _json.Marshal(data)
if err != nil {
panic(err)
}
c.Write(d)
}

View File

@@ -2,12 +2,11 @@
package peppy package peppy
import ( import (
"database/sql" "github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"github.com/gin-gonic/gin"
) )
// GetMatch retrieves general match information. // GetMatch retrieves general match information.
func GetMatch(c *gin.Context, db *sql.DB) { func GetMatch(c *fasthttp.RequestCtx, db *sqlx.DB) {
c.JSON(200, defaultResponse) json(c, 200, defaultResponse)
} }

98
app/peppy/score.go Normal file
View File

@@ -0,0 +1,98 @@
package peppy
import (
"database/sql"
"strconv"
"strings"
"github.com/osuyozora/api/common"
"github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"gopkg.in/thehowl/go-osuapi.v1"
"zxq.co/x/getrank"
)
// GetScores retrieve information about the top 100 scores of a specified beatmap.
func GetScores(c *fasthttp.RequestCtx, db *sqlx.DB) {
if query(c, "b") == "" {
json(c, 200, defaultResponse)
return
}
var beatmapMD5 string
err := db.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", query(c, "b"))
switch {
case err == sql.ErrNoRows:
json(c, 200, defaultResponse)
return
case err != nil:
common.Err(c, err)
json(c, 200, defaultResponse)
return
}
var sb = "scores.score"
if rankable(query(c, "m")) {
sb = "scores.pp"
}
var (
extraWhere string
extraParams []interface{}
)
if query(c, "u") != "" {
w, p := genUser(c, db)
extraWhere = "AND " + w
extraParams = append(extraParams, p)
}
mods := common.Int(query(c, "mods"))
rows, err := db.Query(`
SELECT
scores.id, scores.score, users.username, scores.300_count, scores.100_count,
scores.50_count, scores.misses_count, scores.gekis_count, scores.katus_count,
scores.max_combo, scores.full_combo, scores.mods, users.id, scores.time, scores.pp,
scores.accuracy
FROM scores
INNER JOIN users ON users.id = scores.userid
WHERE scores.completed = '3'
AND users.privileges & 1 > 0
AND scores.beatmap_md5 = ?
AND scores.play_mode = ?
AND scores.mods & ? = ?
`+extraWhere+`
ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, query(c, "limit"), 100, 50)),
append([]interface{}{beatmapMD5, genmodei(query(c, "m")), mods, mods}, extraParams...)...)
if err != nil {
common.Err(c, err)
json(c, 200, defaultResponse)
return
}
var results []osuapi.GSScore
for rows.Next() {
var (
s osuapi.GSScore
fullcombo bool
mods int
date common.UnixTimestamp
accuracy float64
)
err := rows.Scan(
&s.ScoreID, &s.Score.Score, &s.Username, &s.Count300, &s.Count100,
&s.Count50, &s.CountMiss, &s.CountGeki, &s.CountKatu,
&s.MaxCombo, &fullcombo, &mods, &s.UserID, &date, &s.PP,
&accuracy,
)
if err != nil {
if err != sql.ErrNoRows {
common.Err(c, err)
}
continue
}
s.FullCombo = osuapi.OsuBool(fullcombo)
s.Mods = osuapi.Mods(mods)
s.Date = osuapi.MySQLDate(date)
s.Rank = strings.ToUpper(getrank.GetRank(osuapi.Mode(genmodei(query(c, "m"))), s.Mods,
accuracy, s.Count300, s.Count100, s.Count50, s.CountMiss))
results = append(results, s)
}
json(c, 200, results)
return
}

View File

@@ -4,54 +4,61 @@ package peppy
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"strconv"
"git.zxq.co/ripple/ocl" "strings"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"github.com/thehowl/go-osuapi" "github.com/thehowl/go-osuapi"
"github.com/valyala/fasthttp"
"gopkg.in/redis.v5"
"zxq.co/ripple/ocl"
"github.com/osuyozora/api/common"
) )
// R is a redis client.
var R *redis.Client
// GetUser retrieves general user information. // GetUser retrieves general user information.
func GetUser(c *gin.Context, db *sql.DB) { func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
if c.Query("u") == "" { if query(c, "u") == "" {
c.JSON(200, defaultResponse) json(c, 200, defaultResponse)
return return
} }
var user osuapi.User var user osuapi.User
whereClause, p := genUser(c, db) whereClause, p := genUser(c, db)
whereClause = "WHERE " + whereClause whereClause = "WHERE " + whereClause
mode := genmode(c.Query("m")) mode := genmode(query(c, "m"))
var display bool
err := db.QueryRow(fmt.Sprintf( err := db.QueryRow(fmt.Sprintf(
`SELECT `SELECT
users.id, users.username, users.id, users.username,
users_stats.playcount_%s, users_stats.ranked_score_%s, users_stats.total_score_%s, users_stats.playcount_%s, users_stats.ranked_score_%s, users_stats.total_score_%s,
leaderboard_%s.position, users_stats.pp_%s, users_stats.avg_accuracy_%s, users_stats.pp_%s, users_stats.avg_accuracy_%s,
users_stats.country, users_stats.show_country users_stats.country
FROM users FROM users
LEFT JOIN users_stats ON users_stats.id = users.id LEFT JOIN users_stats ON users_stats.id = users.id
LEFT JOIN leaderboard_%s ON leaderboard_%s.user = users.id
%s %s
LIMIT 1`, LIMIT 1`,
mode, mode, mode, mode, mode, mode, mode, mode, whereClause, mode, mode, mode, mode, mode, whereClause,
), p).Scan( ), p).Scan(
&user.UserID, &user.Username, &user.UserID, &user.Username,
&user.Playcount, &user.RankedScore, &user.TotalScore, &user.Playcount, &user.RankedScore, &user.TotalScore,
&user.Rank, &user.PP, &user.Accuracy, &user.PP, &user.Accuracy,
&user.Country, &display, &user.Country,
) )
if err != nil { if err != nil {
c.JSON(200, defaultResponse) json(c, 200, defaultResponse)
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
c.Error(err) common.Err(c, err)
} }
return return
} }
if !display {
user.Country = "XX" user.Rank = int(R.ZRevRank("ripple:leaderboard:"+mode, strconv.Itoa(user.UserID)).Val()) + 1
} user.CountryRank = int(R.ZRevRank("ripple:leaderboard:"+mode+":"+strings.ToLower(user.Country), strconv.Itoa(user.UserID)).Val()) + 1
user.Level = ocl.GetLevelPrecise(user.TotalScore) user.Level = ocl.GetLevelPrecise(user.TotalScore)
c.JSON(200, []osuapi.User{user}) json(c, 200, []osuapi.User{user})
} }

View File

@@ -1,36 +1,35 @@
package peppy package peppy
import ( import (
"database/sql"
"fmt" "fmt"
"strings" "strings"
"time"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
"git.zxq.co/x/getrank" "github.com/valyala/fasthttp"
"github.com/gin-gonic/gin"
"gopkg.in/thehowl/go-osuapi.v1" "gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
) )
// GetUserRecent retrieves an user's recent scores. // GetUserRecent retrieves an user's recent scores.
func GetUserRecent(c *gin.Context, db *sql.DB) { func GetUserRecent(c *fasthttp.RequestCtx, db *sqlx.DB) {
getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, c.Query("limit"), 50, 10)) getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, query(c, "limit"), 50, 10))
} }
// GetUserBest retrieves an user's best scores. // GetUserBest retrieves an user's best scores.
func GetUserBest(c *gin.Context, db *sql.DB) { func GetUserBest(c *fasthttp.RequestCtx, db *sqlx.DB) {
var sb string var sb string
if genmodei(c.Query("m")) == 0 { if rankable(query(c, "m")) {
sb = "scores.pp" sb = "scores.pp"
} else { } else {
sb = "scores.score" sb = "scores.score"
} }
getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, c.Query("limit"), 100, 10)) getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, query(c, "limit"), 100, 10))
} }
func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) { func getUserX(c *fasthttp.RequestCtx, db *sqlx.DB, orderBy string, limit int) {
whereClause, p := genUser(c, db) whereClause, p := genUser(c, db)
query := fmt.Sprintf( sqlQuery := fmt.Sprintf(
`SELECT `SELECT
beatmaps.beatmap_id, scores.score, scores.max_combo, beatmaps.beatmap_id, scores.score, scores.max_combo,
scores.300_count, scores.100_count, scores.50_count, scores.300_count, scores.100_count, scores.50_count,
@@ -40,22 +39,22 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
FROM scores FROM scores
LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5 LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
LEFT JOIN users ON scores.userid = users.id LEFT JOIN users ON scores.userid = users.id
WHERE %s AND scores.play_mode = ? AND users.allowed = '1' WHERE %s AND scores.play_mode = ? AND users.privileges & 1 > 0
%s %s
LIMIT %d`, whereClause, orderBy, limit, LIMIT %d`, whereClause, orderBy, limit,
) )
scores := make([]osuapi.GUSScore, 0, limit) scores := make([]osuapi.GUSScore, 0, limit)
m := genmodei(c.Query("m")) m := genmodei(query(c, "m"))
rows, err := db.Query(query, p, m) rows, err := db.Query(sqlQuery, p, m)
if err != nil { if err != nil {
c.JSON(200, defaultResponse) json(c, 200, defaultResponse)
c.Error(err) common.Err(c, err)
return return
} }
for rows.Next() { for rows.Next() {
var ( var (
curscore osuapi.GUSScore curscore osuapi.GUSScore
rawTime string rawTime common.UnixTimestamp
acc float64 acc float64
fc bool fc bool
mods int mods int
@@ -69,8 +68,8 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
&curscore.PP, &acc, &curscore.PP, &acc,
) )
if err != nil { if err != nil {
c.JSON(200, defaultResponse) json(c, 200, defaultResponse)
c.Error(err) common.Err(c, err)
return return
} }
if bid == nil { if bid == nil {
@@ -80,13 +79,7 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
} }
curscore.FullCombo = osuapi.OsuBool(fc) curscore.FullCombo = osuapi.OsuBool(fc)
curscore.Mods = osuapi.Mods(mods) curscore.Mods = osuapi.Mods(mods)
t, err := time.Parse(common.OsuTimeFormat, rawTime) curscore.Date = osuapi.MySQLDate(rawTime)
if err != nil {
c.JSON(200, defaultResponse)
c.Error(err)
return
}
curscore.Date = osuapi.MySQLDate(t)
curscore.Rank = strings.ToUpper(getrank.GetRank( curscore.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(m), osuapi.Mode(m),
curscore.Mods, curscore.Mods,
@@ -98,5 +91,5 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
)) ))
scores = append(scores, curscore) scores = append(scores, curscore)
} }
c.JSON(200, scores) json(c, 200, scores)
} }

View File

@@ -1,14 +1,17 @@
package app package app
import ( import (
"database/sql" "github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"github.com/gin-gonic/gin"
) )
// PeppyMethod generates a method for the peppyapi // PeppyMethod generates a method for the peppyapi
func PeppyMethod(a func(c *gin.Context, db *sql.DB)) gin.HandlerFunc { func PeppyMethod(a func(c *fasthttp.RequestCtx, db *sqlx.DB)) fasthttp.RequestHandler {
return func(c *gin.Context) { return func(c *fasthttp.RequestCtx) {
doggo.Incr("requests.peppy", nil, 1)
c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
// I have no idea how, but I manged to accidentally string the first 4 // I have no idea how, but I manged to accidentally string the first 4
// letters of the alphabet into a single function call. // letters of the alphabet into a single function call.
a(c, db) a(c, db)

106
app/router.go Normal file
View File

@@ -0,0 +1,106 @@
package app
import (
"bytes"
"errors"
"fmt"
"time"
"github.com/buaazp/fasthttprouter"
"github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"github.com/osuyozora/api/common"
)
type router struct {
r *fasthttprouter.Router
}
func (r router) Method(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
r.r.GET(path, wrap(Method(f, privilegesNeeded...)))
}
func (r router) POSTMethod(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
r.r.POST(path, wrap(Method(f, privilegesNeeded...)))
}
func (r router) Peppy(path string, a func(c *fasthttp.RequestCtx, db *sqlx.DB)) {
r.r.GET(path, wrap(PeppyMethod(a)))
}
func (r router) GET(path string, handle fasthttp.RequestHandler) {
r.r.GET(path, wrap(handle))
}
func (r router) PlainGET(path string, handle fasthttp.RequestHandler) {
r.r.GET(path, handle)
}
const (
// \x1b is escape code for ESC
// <ESC>[<n>m is escape sequence for a certain colour
// no IP is written out because of the hundreds of possible ways to pass IPs
// to a request when using a reverse proxy
// this is partly inspired from gin, though made even more simplistic.
fmtString = "%s | %15s |\x1b[%sm %3d \x1b[0m %-7s %s\n"
// a kind of human readable RFC3339
timeFormat = "2006-01-02 15:04:05"
// color reference
// http://misc.flogisoft.com/bash/tip_colors_and_formatting
colorOk = "42" // green
colorError = "41" // red
)
// wrap returns a function that wraps around handle, providing middleware
// functionality to apply to all API calls, which is to say:
// - logging
// - panic recovery (reporting to sentry)
// - gzipping
func wrap(handle fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(c *fasthttp.RequestCtx) {
start := time.Now()
doggo.Incr("requests", nil, 1)
defer func() {
if rval := recover(); rval != nil {
var err error
switch rval := rval.(type) {
case string:
err = errors.New(rval)
case error:
err = rval
default:
err = fmt.Errorf("%v - %#v", rval, rval)
}
common.Err(c, err)
c.SetStatusCode(500)
c.SetBodyString(`{ "code": 500, "message": "something really bad happened" }`)
}
// switch color to colorError if statusCode is in [500;600)
color := colorOk
statusCode := c.Response.StatusCode()
if statusCode >= 500 && statusCode < 600 {
color = colorError
}
if bytes.Contains(c.Request.Header.Peek("Accept-Encoding"), s2b("gzip")) {
c.Response.Header.Add("Content-Encoding", "gzip")
c.Response.Header.Add("Vary", "Accept-Encoding")
b := c.Response.Body()
c.Response.ResetBody()
fasthttp.WriteGzip(c.Response.BodyWriter(), b)
}
// print stuff
fmt.Printf(
fmtString,
time.Now().Format(timeFormat),
time.Since(start).String(),
color,
statusCode,
c.Method(),
c.Path(),
)
}()
handle(c)
}
}

View File

@@ -1,97 +1,186 @@
package app package app
import ( import (
"database/sql" "fmt"
"time"
"git.zxq.co/ripple/rippleapi/app/internals" "github.com/DataDog/datadog-go/statsd"
"git.zxq.co/ripple/rippleapi/app/peppy" fhr "github.com/buaazp/fasthttprouter"
"git.zxq.co/ripple/rippleapi/app/v1" "github.com/getsentry/raven-go"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
"github.com/gin-gonic/contrib/gzip" "gopkg.in/redis.v5"
"github.com/gin-gonic/gin" "github.com/osuyozora/api/app/internals"
"github.com/osuyozora/api/app/peppy"
"github.com/osuyozora/api/app/v1"
"github.com/osuyozora/api/app/websockets"
"github.com/osuyozora/api/common"
) )
var db *sql.DB var (
db *sqlx.DB
cf common.Conf
doggo *statsd.Client
red *redis.Client
)
// Start begins taking HTTP connections. // Start begins taking HTTP connections.
func Start(conf common.Conf, dbO *sql.DB) *gin.Engine { func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
db = dbO db = dbO
r := gin.Default() cf = conf
r.Use(gzip.Gzip(gzip.DefaultCompression), ErrorHandler())
api := r.Group("/api") rawRouter := fhr.New()
r := router{rawRouter}
// TODO: add back gzip
// TODO: add logging
// TODO: add sentry panic recovering
// sentry
if conf.SentryDSN != "" {
ravenClient, err := raven.New(conf.SentryDSN)
ravenClient.SetRelease(common.Version)
if err != nil {
fmt.Println(err)
} else {
// r.Use(Recovery(ravenClient, false))
common.RavenClient = ravenClient
}
}
// datadog
var err error
doggo, err = statsd.New("127.0.0.1:8125")
if err != nil {
fmt.Println(err)
}
doggo.Namespace = "api."
// redis
red = redis.NewClient(&redis.Options{
Addr: conf.RedisAddr,
Password: conf.RedisPassword,
DB: conf.RedisDB,
})
peppy.R = red
// token updater
go tokenUpdater(db)
// start websocket
websockets.Start(red, db)
// start load achievements
go v1.LoadAchievementsEvery(db, time.Minute*10)
// peppyapi
{ {
gv1 := api.Group("/v1") r.Peppy("/api/get_user", peppy.GetUser)
r.Peppy("/api/get_match", peppy.GetMatch)
r.Peppy("/api/get_user_recent", peppy.GetUserRecent)
r.Peppy("/api/get_user_best", peppy.GetUserBest)
r.Peppy("/api/get_scores", peppy.GetScores)
r.Peppy("/api/get_beatmaps", peppy.GetBeatmap)
}
// v1 API
{ {
gv1.POST("/tokens/new", Method(v1.TokenNewPOST)) r.POSTMethod("/api/v1/tokens/self/delete", v1.TokenSelfDeletePOST)
gv1.GET("/tokens/self/delete", Method(v1.TokenSelfDeleteGET))
// Auth-free API endpoints // Auth-free API endpoints (public data)
gv1.GET("/ping", Method(v1.PingGET)) r.Method("/api/v1/ping", v1.PingGET)
gv1.GET("/surprise_me", Method(v1.SurpriseMeGET)) r.Method("/api/v1/surprise_me", v1.SurpriseMeGET)
gv1.GET("/privileges", Method(v1.PrivilegesGET)) r.Method("/api/v1/users", v1.UsersGET)
gv1.GET("/doc", Method(v1.DocGET)) r.Method("/api/v1/users/whatid", v1.UserWhatsTheIDGET)
gv1.GET("/doc/content", Method(v1.DocContentGET)) r.Method("/api/v1/users/full", v1.UserFullGET)
gv1.GET("/doc/rules", Method(v1.DocRulesGET)) r.Method("/api/v1/users/rx/full", v1.UserFullGETRx)
r.Method("/api/v1/users/ap/full", v1.UserFullGETAp)
r.Method("/api/v1/users/achievements", v1.UserAchievementsGET)
r.Method("/api/v1/users/userpage", v1.UserUserpageGET)
r.Method("/api/v1/users/lookup", v1.UserLookupGET)
r.Method("/api/v1/users/scores/best", v1.UserScoresBestGET)
r.Method("/api/v1/users/scores/recent", v1.UserScoresRecentGET)
r.Method("/api/v1/badges", v1.BadgesGET)
r.Method("/api/v1/badges/members", v1.BadgeMembersGET)
r.Method("/api/v1/clans", v1.ClansGET)
r.Method("/api/v1/clans/members", v1.ClanMembersGET)
r.Method("/api/v1/clans/stats", v1.TotalClanStatsGET)
r.Method("/api/v1/clans/stats/all", v1.AllClanStatsGET)
r.Method("/api/v1/clans/getinvite", v1.ClanInviteGET)
r.Method("/api/v1/clans/isclan", v1.IsInClanGET)
r.Method("/api/v1/beatmaps", v1.BeatmapGET)
r.Method("/api/v1/leaderboard", v1.LeaderboardGET)
r.Method("/api/v1/relaxboard", v1.LeaderboardRxGET)
r.Method("/api/v1/autoboard", v1.LeaderboardApGET)
r.Method("/api/v1/tokens", v1.TokenGET)
r.Method("/api/v1/users/self", v1.UserSelfGET)
r.Method("/api/v1/tokens/self", v1.TokenSelfGET)
r.Method("/api/v1/blog/posts", v1.BlogPostsGET)
r.Method("/api/v1/scores", v1.ScoresGET)
r.Method("/api/v1/beatmaps/rank_requests/status", v1.BeatmapRankRequestsStatusGET)
// Read privilege required // Other leaderboard memes
gv1.GET("/users", Method(v1.UsersGET, common.PrivilegeRead)) r.Method("/api/v1/users/scores/relax/best", v1.UserScoresBestRelaxGET)
gv1.GET("/users/self", Method(v1.UserSelfGET, common.PrivilegeRead)) r.Method("/api/v1/users/scores/relax/recent", v1.UserScoresRecentRelaxGET)
gv1.GET("/users/whatid", Method(v1.UserWhatsTheIDGET, common.PrivilegeRead)) r.Method("/api/v1/users/scores/ap/best", v1.UserScoresBestAPGET)
gv1.GET("/users/full", Method(v1.UserFullGET, common.PrivilegeRead)) r.Method("/api/v1/users/scores/ap/recent", v1.UserScoresRecentAPGET)
gv1.GET("/users/userpage", Method(v1.UserUserpageGET, common.PrivilegeRead))
gv1.GET("/users/lookup", Method(v1.UserLookupGET, common.PrivilegeRead))
gv1.GET("/users/scores/best", Method(v1.UserScoresBestGET, common.PrivilegeRead))
gv1.GET("/users/scores/recent", Method(v1.UserScoresRecentGET, common.PrivilegeRead))
gv1.GET("/badges", Method(v1.BadgesGET, common.PrivilegeRead))
gv1.GET("/beatmaps", Method(v1.BeatmapGET, common.PrivilegeRead))
gv1.GET("/leaderboard", Method(v1.LeaderboardGET, common.PrivilegeRead))
gv1.GET("/tokens", Method(v1.TokenGET, common.PrivilegeRead))
gv1.GET("/tokens/self", Method(v1.TokenSelfGET, common.PrivilegeRead))
// ReadConfidential privilege required // ReadConfidential privilege required
gv1.GET("/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential)) r.Method("/api/v1/friends", v1.FriendsGET, common.PrivilegeReadConfidential)
gv1.GET("/friends/with", Method(v1.FriendsWithGET, common.PrivilegeReadConfidential)) r.Method("/api/v1/friends/with", v1.FriendsWithGET, common.PrivilegeReadConfidential)
r.Method("/api/v1/users/self/donor_info", v1.UsersSelfDonorInfoGET, common.PrivilegeReadConfidential)
r.Method("/api/v1/users/self/favourite_mode", v1.UsersSelfFavouriteModeGET, common.PrivilegeReadConfidential)
r.Method("/api/v1/users/self/settings", v1.UsersSelfSettingsGET, common.PrivilegeReadConfidential)
// Write privilege required // Write privilege required
gv1.GET("/friends/add", Method(v1.FriendsAddGET, common.PrivilegeWrite)) r.POSTMethod("/api/v1/friends/add", v1.FriendsAddPOST, common.PrivilegeWrite)
gv1.GET("/friends/del", Method(v1.FriendsDelGET, common.PrivilegeWrite)) r.POSTMethod("/api/v1/friends/del", v1.FriendsDelPOST, common.PrivilegeWrite)
r.POSTMethod("/api/v1/users/self/settings", v1.UsersSelfSettingsPOST, common.PrivilegeWrite)
r.POSTMethod("/api/v1/users/self/userpage", v1.UserSelfUserpagePOST, common.PrivilegeWrite)
r.POSTMethod("/api/v1/beatmaps/rank_requests", v1.BeatmapRankRequestsSubmitPOST, common.PrivilegeWrite)
// Admin: RAP
r.POSTMethod("/api/v1/rap/log", v1.RAPLogPOST)
// Admin: beatmap // Admin: beatmap
gv1.POST("/beatmaps/set_status", Method(v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap)) r.POSTMethod("/api/v1/beatmaps/set_status", v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap)
gv1.GET("/beatmaps/ranked_frozen_full", Method(v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap)) r.Method("/api/v1/beatmaps/ranked_frozen_full", v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap)
// Admin: user managing // Admin: user managing
gv1.POST("/users/manage/set_allowed", Method(v1.UserManageSetAllowedPOST, common.PrivilegeManageUser)) r.POSTMethod("/api/v1/users/manage/set_allowed", v1.UserManageSetAllowedPOST, common.PrivilegeManageUser)
r.POSTMethod("/api/v1/users/edit", v1.UserEditPOST, common.PrivilegeManageUser)
r.POSTMethod("/api/v1/users/wipe", v1.WipeUserPOST, common.PrivilegeManageUser)
r.POSTMethod("/api/v1/scores/reports", v1.ScoreReportPOST, common.PrivilegeManageUser)
// M E T A // M E T A
// E T "wow thats so meta" // E T "wow thats so meta"
// T E -- the one who said "wow thats so meta" // T E -- the one who said "wow thats so meta"
// A T E M // A T E M
gv1.GET("/meta/restart", Method(v1.MetaRestartGET, common.PrivilegeAPIMeta)) r.Method("/api/v1/meta/restart", v1.MetaRestartGET, common.PrivilegeAPIMeta)
gv1.GET("/meta/kill", Method(v1.MetaKillGET, common.PrivilegeAPIMeta)) r.Method("/api/v1/meta/up_since", v1.MetaUpSinceGET, common.PrivilegeAPIMeta)
gv1.GET("/meta/up_since", Method(v1.MetaUpSinceGET, common.PrivilegeAPIMeta)) r.Method("/api/v1/meta/update", v1.MetaUpdateGET, common.PrivilegeAPIMeta)
gv1.GET("/meta/update", Method(v1.MetaUpdateGET, common.PrivilegeAPIMeta))
// User Managing + meta // User Managing + meta
gv1.GET("/tokens/fix_privileges", Method(v1.TokenFixPrivilegesGET, r.POSTMethod("/api/v1/tokens/fix_privileges", v1.TokenFixPrivilegesPOST,
common.PrivilegeManageUser, common.PrivilegeAPIMeta)) common.PrivilegeManageUser, common.PrivilegeAPIMeta)
} }
api.GET("/status", internals.Status) // Websocket API
{
// peppyapi r.PlainGET("/api/v1/ws", websockets.WebsocketV1Entry)
api.GET("/get_user", PeppyMethod(peppy.GetUser))
api.GET("/get_match", PeppyMethod(peppy.GetMatch))
api.GET("/get_user_recent", PeppyMethod(peppy.GetUserRecent))
api.GET("/get_user_best", PeppyMethod(peppy.GetUserBest))
} }
r.NoRoute(v1.Handle404) // in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
{
r.Peppy("/api/v1/get_user", peppy.GetUser)
r.Peppy("/api/v1/get_match", peppy.GetMatch)
r.Peppy("/api/v1/get_user_recent", peppy.GetUserRecent)
r.Peppy("/api/v1/get_user_best", peppy.GetUserBest)
r.Peppy("/api/v1/get_scores", peppy.GetScores)
r.Peppy("/api/v1/get_beatmaps", peppy.GetBeatmap)
}
return r r.GET("/api/status", internals.Status)
/*if conf.Unix {
panic(r.RunUnix(conf.ListenTo)) rawRouter.NotFound = v1.Handle404
}
panic(r.Run(conf.ListenTo))*/ return rawRouter
} }

View File

@@ -2,26 +2,39 @@ package app
import ( import (
"crypto/md5" "crypto/md5"
"crypto/sha256"
"database/sql" "database/sql"
"fmt" "fmt"
"time"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
"github.com/osuyozora/api/common"
) )
// GetTokenFull retrieves an user ID and their token privileges knowing their API token. // GetTokenFull retrieves an user ID and their token privileges knowing their API token.
func GetTokenFull(token string, db *sql.DB) (common.Token, bool) { func GetTokenFull(token string, db *sqlx.DB) (common.Token, bool) {
var t common.Token var (
var privs uint64 t common.Token
var priv8 bool tokenPrivsRaw uint64
err := db.QueryRow("SELECT id, user, privileges, private FROM tokens WHERE token = ? LIMIT 1", userPrivsRaw uint64
priv8 bool
)
err := db.QueryRow(`SELECT
t.id, t.user, t.privileges, t.private, u.privileges
FROM tokens t
LEFT JOIN users u ON u.id = t.user
WHERE token = ? LIMIT 1`,
fmt.Sprintf("%x", md5.Sum([]byte(token)))). fmt.Sprintf("%x", md5.Sum([]byte(token)))).
Scan( Scan(
&t.ID, &t.UserID, &privs, &priv8, &t.ID, &t.UserID, &tokenPrivsRaw, &priv8, &userPrivsRaw,
) )
t.Privileges = common.Privileges(privs) updateTokens <- t.ID
if priv8 { if priv8 {
privs = common.PrivilegeRead | common.PrivilegeReadConfidential | common.PrivilegeWrite // all privileges, they'll get removed by canOnly anyway.
tokenPrivsRaw = (common.PrivilegeBeatmap << 1) - 1
} }
t.UserPrivileges = common.UserPrivileges(userPrivsRaw)
t.TokenPrivileges = common.Privileges(tokenPrivsRaw).CanOnly(t.UserPrivileges)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return common.Token{}, false return common.Token{}, false
@@ -32,3 +45,68 @@ func GetTokenFull(token string, db *sql.DB) (common.Token, bool) {
return t, true return t, true
} }
} }
var updateTokens = make(chan int, 100)
func tokenUpdater(db *sqlx.DB) {
for {
// prepare a queue of tokens to update.
tokensToUpdate := make([]int, 0, 50)
AwaitLoop:
for {
// if we got ten, move on and update
if len(tokensToUpdate) >= 50 {
break
}
// if we ain't got any, add what we get straight from updateTokens
if len(tokensToUpdate) == 0 {
x := <-updateTokens
tokensToUpdate = append(tokensToUpdate, x)
continue
}
// otherwise, wait from updateTokens with a timeout of 10 seconds
select {
case x := <-updateTokens:
tokensToUpdate = append(tokensToUpdate, x)
case <-time.After(10 * time.Second):
// wondering what this means?
// https://golang.org/ref/spec#Break_statements
break AwaitLoop
}
}
q, a, _ := sqlx.In("UPDATE tokens SET last_updated = ? WHERE id IN (?)", time.Now().Unix(), tokensToUpdate)
q = db.Rebind(q)
_, err := db.Exec(q, a...)
if err != nil {
fmt.Println(err)
}
}
}
// BearerToken parses a Token given in the Authorization header, with the
// Bearer prefix.
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
var x struct {
Scope string
Extra int
}
db.Get(&x, "SELECT scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
if x.Extra == 0 {
return common.Token{}, false
}
var privs uint64
db.Get(&privs, "SELECT privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
var t common.Token
t.ID = -1
t.UserID = x.Extra
t.Value = token
t.UserPrivileges = common.UserPrivileges(privs)
t.TokenPrivileges = common.OAuthPrivileges(x.Scope).CanOnly(t.UserPrivileges)
return t, true
}

View File

@@ -1,8 +1,10 @@
package v1 package v1
import ( import (
"git.zxq.co/ripple/rippleapi/common" "encoding/json"
"github.com/gin-gonic/gin"
"github.com/valyala/fasthttp"
"github.com/osuyozora/api/common"
) )
type response404 struct { type response404 struct {
@@ -11,11 +13,17 @@ type response404 struct {
} }
// Handle404 handles requests with no implemented handlers. // Handle404 handles requests with no implemented handlers.
func Handle404(c *gin.Context) { func Handle404(c *fasthttp.RequestCtx) {
c.IndentedJSON(404, response404{ c.Response.Header.Add("X-Real-404", "yes")
data, err := json.MarshalIndent(response404{
ResponseBase: common.ResponseBase{ ResponseBase: common.ResponseBase{
Code: 404, Code: 404,
}, },
Cats: surpriseMe(), Cats: surpriseMe(),
}) }, "", "\t")
if err != nil {
panic(err)
}
c.SetStatusCode(404)
c.Write(data)
} }

View File

@@ -3,11 +3,11 @@ package v1
import ( import (
"database/sql" "database/sql"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
type singleBadge struct { type singleBadge struct {
ID int `json:"id"` ID int `json:"id,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Icon string `json:"icon"` Icon string `json:"icon"`
} }
@@ -24,11 +24,10 @@ func BadgesGET(md common.MethodData) common.CodeMessager {
rows *sql.Rows rows *sql.Rows
err error err error
) )
if md.C.Query("id") != "" { if md.Query("id") != "" {
// TODO(howl): ID validation rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ? LIMIT 1", md.Query("id"))
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ?", md.C.Query("id"))
} else { } else {
rows, err = md.DB.Query("SELECT id, name, icon FROM badges") rows, err = md.DB.Query("SELECT id, name, icon FROM badges " + common.Paginate(md.Query("p"), md.Query("l"), 50))
} }
if err != nil { if err != nil {
md.Err(err) md.Err(err)
@@ -49,3 +48,35 @@ func BadgesGET(md common.MethodData) common.CodeMessager {
r.ResponseBase.Code = 200 r.ResponseBase.Code = 200
return r return r
} }
type badgeMembersData struct {
common.ResponseBase
Members []userData `json:"members"`
}
// BadgeMembersGET retrieves the people who have a certain badge.
func BadgeMembersGET(md common.MethodData) common.CodeMessager {
i := common.Int(md.Query("id"))
if i == 0 {
return ErrMissingField("id")
}
var members badgeMembersData
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country
FROM user_badges ub
INNER JOIN users ON users.id = ub.user
INNER JOIN users_stats ON users_stats.id = ub.user
WHERE badge = ?
ORDER BY id ASC `+common.Paginate(md.Query("p"), md.Query("l"), 50), i)
if err != nil {
md.Err(err)
return Err500
}
members.Code = 200
return members
}

View File

@@ -2,11 +2,17 @@ package v1
import ( import (
"database/sql" "database/sql"
"time"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
type difficulty struct {
STD float64 `json:"std"`
Taiko float64 `json:"taiko"`
CTB float64 `json:"ctb"`
Mania float64 `json:"mania"`
}
type beatmap struct { type beatmap struct {
BeatmapID int `json:"beatmap_id"` BeatmapID int `json:"beatmap_id"`
BeatmapsetID int `json:"beatmapset_id"` BeatmapsetID int `json:"beatmapset_id"`
@@ -15,46 +21,12 @@ type beatmap struct {
AR float32 `json:"ar"` AR float32 `json:"ar"`
OD float32 `json:"od"` OD float32 `json:"od"`
Difficulty float64 `json:"difficulty"` Difficulty float64 `json:"difficulty"`
Diff2 difficulty `json:"difficulty2"` // fuck nyo
MaxCombo int `json:"max_combo"` MaxCombo int `json:"max_combo"`
HitLength int `json:"hit_length"` HitLength int `json:"hit_length"`
Ranked int `json:"ranked"` Ranked int `json:"ranked"`
RankedStatusFrozen int `json:"ranked_status_frozen"` RankedStatusFrozen int `json:"ranked_status_frozen"`
LatestUpdate time.Time `json:"latest_update"` LatestUpdate common.UnixTimestamp `json:"latest_update"`
}
type beatmapMayOrMayNotExist struct {
BeatmapID *int
BeatmapsetID *int
BeatmapMD5 *string
SongName *string
AR *float32
OD *float32
Difficulty *float64
MaxCombo *int
HitLength *int
Ranked *int
RankedStatusFrozen *int
LatestUpdate *time.Time
}
func (b *beatmapMayOrMayNotExist) toBeatmap() *beatmap {
if b == nil || b.BeatmapID == nil {
return nil
}
return &beatmap{
BeatmapID: *b.BeatmapID,
BeatmapsetID: *b.BeatmapsetID,
BeatmapMD5: *b.BeatmapMD5,
SongName: *b.SongName,
AR: *b.AR,
OD: *b.OD,
Difficulty: *b.Difficulty,
MaxCombo: *b.MaxCombo,
HitLength: *b.HitLength,
Ranked: *b.Ranked,
RankedStatusFrozen: *b.RankedStatusFrozen,
LatestUpdate: *b.LatestUpdate,
}
} }
type beatmapResponse struct { type beatmapResponse struct {
@@ -77,10 +49,10 @@ type beatmapSetStatusData struct {
// the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16 // the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16
func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager { func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
var req beatmapSetStatusData var req beatmapSetStatusData
md.RequestData.Unmarshal(&req) md.Unmarshal(&req)
var miss []string var miss []string
if req.BeatmapsetID == 0 && req.BeatmapID == 0 { if req.BeatmapsetID <= 0 && req.BeatmapID <= 0 {
miss = append(miss, "beatmapset_id or beatmap_id") miss = append(miss, "beatmapset_id or beatmap_id")
} }
if len(miss) != 0 { if len(miss) != 0 {
@@ -110,74 +82,99 @@ func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
SET ranked = ?, ranked_status_freezed = ? SET ranked = ?, ranked_status_freezed = ?
WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param) WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param)
return getSet(md, param) if req.BeatmapID > 0 {
md.Ctx.Request.URI().QueryArgs().SetUint("bb", req.BeatmapID)
} else {
md.Ctx.Request.URI().QueryArgs().SetUint("s", req.BeatmapsetID)
}
return getMultipleBeatmaps(md)
} }
// BeatmapGET retrieves a beatmap. // BeatmapGET retrieves a beatmap.
func BeatmapGET(md common.MethodData) common.CodeMessager { func BeatmapGET(md common.MethodData) common.CodeMessager {
if md.C.Query("s") == "" && md.C.Query("b") == "" { beatmapID := common.Int(md.Query("b"))
return common.SimpleResponse(400, "Must pass either querystring param 'b' or 's'")
}
setID := common.Int(md.C.Query("s"))
if setID != 0 {
return getSet(md, setID)
}
beatmapID := common.Int(md.C.Query("b"))
if beatmapID != 0 { if beatmapID != 0 {
return getBeatmap(md, beatmapID) return getBeatmapSingle(md, beatmapID)
} }
return common.SimpleResponse(400, "Please pass either a valid beatmapset ID or a valid beatmap ID") return getMultipleBeatmaps(md)
} }
const baseBeatmapSelect = ` const baseBeatmapSelect = `
SELECT SELECT
beatmap_id, beatmapset_id, beatmap_md5, beatmap_id, beatmapset_id, beatmap_md5,
song_name, ar, od, difficulty, max_combo, song_name, ar, od, difficulty_std, difficulty_taiko,
difficulty_ctb, difficulty_mania, max_combo,
hit_length, ranked, ranked_status_freezed, hit_length, ranked, ranked_status_freezed,
latest_update latest_update
FROM beatmaps FROM beatmaps
` `
func getSet(md common.MethodData, setID int) common.CodeMessager { func getMultipleBeatmaps(md common.MethodData) common.CodeMessager {
rows, err := md.DB.Query(baseBeatmapSelect+"WHERE beatmapset_id = ?", setID) sort := common.Sort(md, common.SortConfiguration{
Allowed: []string{
"beatmapset_id",
"beatmap_id",
"id",
"ar",
"od",
"difficulty_std",
"difficulty_taiko",
"difficulty_ctb",
"difficulty_mania",
"max_combo",
"latest_update",
"playcount",
"passcount",
},
Default: "id DESC",
Table: "beatmaps",
})
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
where := common.
Where("song_name = ?", md.Query("song_name")).
Where("ranked_status_freezed = ?", md.Query("ranked_status_frozen"), "0", "1").
In("beatmap_id", pm("bb")...).
In("beatmapset_id", pm("s")...).
In("beatmap_md5", pm("md5")...)
rows, err := md.DB.Query(baseBeatmapSelect+
where.Clause+" "+sort+" "+
common.Paginate(md.Query("p"), md.Query("l"), 50), where.Params...)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
var r beatmapSetResponse var r beatmapSetResponse
for rows.Next() { for rows.Next() {
var ( var b beatmap
b beatmap
rawLatestUpdate int64
)
err = rows.Scan( err = rows.Scan(
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
&b.SongName, &b.AR, &b.OD, &b.Difficulty, &b.MaxCombo, &b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen, &b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
&rawLatestUpdate, &b.LatestUpdate,
) )
b.Difficulty = b.Diff2.STD
if err != nil { if err != nil {
md.Err(err) md.Err(err)
continue continue
} }
b.LatestUpdate = time.Unix(rawLatestUpdate, 0)
r.Beatmaps = append(r.Beatmaps, b) r.Beatmaps = append(r.Beatmaps, b)
} }
r.Code = 200 r.Code = 200
return r return r
} }
func getBeatmap(md common.MethodData, beatmapID int) common.CodeMessager { func getBeatmapSingle(md common.MethodData, beatmapID int) common.CodeMessager {
var ( var b beatmap
b beatmap
rawLatestUpdate int64
)
err := md.DB.QueryRow(baseBeatmapSelect+"WHERE beatmap_id = ? LIMIT 1", beatmapID).Scan( err := md.DB.QueryRow(baseBeatmapSelect+"WHERE beatmap_id = ? LIMIT 1", beatmapID).Scan(
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
&b.SongName, &b.AR, &b.OD, &b.Difficulty, &b.MaxCombo, &b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen, &b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
&rawLatestUpdate, &b.LatestUpdate,
) )
b.Difficulty = b.Diff2.STD
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return common.SimpleResponse(404, "That beatmap could not be found!") return common.SimpleResponse(404, "That beatmap could not be found!")
@@ -185,7 +182,6 @@ func getBeatmap(md common.MethodData, beatmapID int) common.CodeMessager {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
b.LatestUpdate = time.Unix(rawLatestUpdate, 0)
var r beatmapResponse var r beatmapResponse
r.Code = 200 r.Code = 200
r.beatmap = b r.beatmap = b

159
app/v1/beatmap_requests.go Normal file
View File

@@ -0,0 +1,159 @@
package v1
import (
"database/sql"
"encoding/json"
"strconv"
"time"
"github.com/osuyozora/api/common"
"github.com/osuyozora/api/limit"
)
type rankRequestsStatusResponse struct {
common.ResponseBase
QueueSize int `json:"queue_size"`
MaxPerUser int `json:"max_per_user"`
Submitted int `json:"submitted"`
SubmittedByUser *int `json:"submitted_by_user,omitempty"`
CanSubmit *bool `json:"can_submit,omitempty"`
NextExpiration *time.Time `json:"next_expiration"`
}
// BeatmapRankRequestsStatusGET gets the current status for beatmap ranking requests.
func BeatmapRankRequestsStatusGET(md common.MethodData) common.CodeMessager {
c := common.GetConf()
rows, err := md.DB.Query("SELECT userid, time FROM rank_requests WHERE time > ? ORDER BY id ASC LIMIT "+strconv.Itoa(c.RankQueueSize), time.Now().Add(-time.Hour*24).Unix())
if err != nil {
md.Err(err)
return Err500
}
var r rankRequestsStatusResponse
// if it's not auth-free access and we have got ReadConfidential, we can
// know if this user can submit beatmaps or not.
hasConfid := md.ID() != 0 && md.User.TokenPrivileges&common.PrivilegeReadConfidential > 0
if hasConfid {
r.SubmittedByUser = new(int)
}
isFirst := true
for rows.Next() {
var (
user int
timestamp common.UnixTimestamp
)
err := rows.Scan(&user, &timestamp)
if err != nil {
md.Err(err)
continue
}
// if the user submitted this rank request, increase the number of
// rank requests submitted by this user
if user == md.ID() && r.SubmittedByUser != nil {
(*r.SubmittedByUser)++
}
// also, if this is the first result, it means it will be the next to
// expire.
if isFirst {
x := time.Time(timestamp)
r.NextExpiration = &x
isFirst = false
}
r.Submitted++
}
r.QueueSize = c.RankQueueSize
r.MaxPerUser = c.BeatmapRequestsPerUser
if hasConfid {
x := r.Submitted < r.QueueSize && *r.SubmittedByUser < r.MaxPerUser
r.CanSubmit = &x
}
r.Code = 200
return r
}
type submitRequestData struct {
ID int `json:"id"`
SetID int `json:"set_id"`
}
// BeatmapRankRequestsSubmitPOST submits a new beatmap for ranking approval.
func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
var d submitRequestData
err := md.Unmarshal(&d)
if err != nil {
return ErrBadJSON
}
// check json data is present
if d.ID == 0 && d.SetID == 0 {
return ErrMissingField("id|set_id")
}
// you've been rate limited
if !limit.NonBlockingRequest("rankrequest:u:"+strconv.Itoa(md.ID()), 5) {
return common.SimpleResponse(429, "You may only try to request 5 beatmaps per minute.")
}
// find out from BeatmapRankRequestsStatusGET if we can submit beatmaps.
statusRaw := BeatmapRankRequestsStatusGET(md)
status, ok := statusRaw.(rankRequestsStatusResponse)
if !ok {
// if it's not a rankRequestsStatusResponse, it means it's an error
return statusRaw
}
if !*status.CanSubmit {
return common.SimpleResponse(403, "It's not possible to do a rank request at this time.")
}
w := common.
Where("beatmap_id = ?", strconv.Itoa(d.ID)).Or().
Where("beatmapset_id = ?", strconv.Itoa(d.SetID))
var ranked int
err = md.DB.QueryRow("SELECT ranked FROM beatmaps "+w.Clause+" LIMIT 1", w.Params...).Scan(&ranked)
if ranked >= 2 {
return common.SimpleResponse(406, "That beatmap is already ranked.")
}
switch err {
case nil:
// move on
case sql.ErrNoRows:
data, _ := json.Marshal(d)
md.R.Publish("lets:beatmap_updates", string(data))
default:
md.Err(err)
return Err500
}
// type and value of beatmap rank request
t := "b"
v := d.ID
if d.SetID != 0 {
t = "s"
v = d.SetID
}
err = md.DB.QueryRow("SELECT 1 FROM rank_requests WHERE bid = ? AND type = ? AND time > ?",
v, t, time.Now().Add(-time.Hour*24).Unix()).Scan(new(int))
// error handling
switch err {
case sql.ErrNoRows:
break
case nil:
// we're returning a success because if the request was already sent in the past 24
// hours, it's as if the user submitted it.
return BeatmapRankRequestsStatusGET(md)
default:
md.Err(err)
return Err500
}
_, err = md.DB.Exec(
"INSERT INTO rank_requests (userid, bid, type, time, blacklisted) VALUES (?, ?, ?, ?, 0)",
md.ID(), v, t, time.Now().Unix())
if err != nil {
md.Err(err)
return Err500
}
return BeatmapRankRequestsStatusGET(md)
}

184
app/v1/blog.go Normal file
View File

@@ -0,0 +1,184 @@
package v1
import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/osuyozora/api/common"
)
// This basically proxies requests from Medium's API and is used on Ripple's
// home page to display the latest blog posts.
type mediumResp struct {
Success bool `json:"success"`
Payload struct {
Posts []mediumPost `json:"posts"`
References struct {
User map[string]mediumUser
} `json:"references"`
} `json:"payload"`
}
type mediumPost struct {
ID string `json:"id"`
CreatorID string `json:"creatorId"`
Title string `json:"title"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
Virtuals mediumPostVirtuals `json:"virtuals"`
ImportedURL string `json:"importedUrl"`
UniqueSlug string `json:"uniqueSlug"`
}
type mediumUser struct {
UserID string `json:"userId"`
Name string `json:"name"`
Username string `json:"username"`
}
type mediumPostVirtuals struct {
Subtitle string `json:"subtitle"`
WordCount int `json:"wordCount"`
ReadingTime float64 `json:"readingTime"`
}
// there's gotta be a better way
type blogPost struct {
ID string `json:"id"`
Creator blogUser `json:"creator"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ImportedURL string `json:"imported_url"`
UniqueSlug string `json:"unique_slug"`
Snippet string `json:"snippet"`
WordCount int `json:"word_count"`
ReadingTime float64 `json:"reading_time"`
}
type blogUser struct {
UserID string `json:"user_id"`
Name string `json:"name"`
Username string `json:"username"`
}
type blogPostsResponse struct {
common.ResponseBase
Posts []blogPost `json:"posts"`
}
// consts for the medium API
const (
mediumAPIResponsePrefix = `])}while(1);</x>`
mediumAPIAllPosts = `https://blog.ripple.moe/latest?format=json`
)
func init() {
gob.Register([]blogPost{})
}
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
func BlogPostsGET(md common.MethodData) common.CodeMessager {
// check if posts are cached in redis
res := md.R.Get("api:blog_posts").Val()
if res != "" {
// decode values
posts := make([]blogPost, 0, 20)
err := gob.NewDecoder(strings.NewReader(res)).Decode(&posts)
if err != nil {
md.Err(err)
return Err500
}
// create response and return
var r blogPostsResponse
r.Code = 200
r.Posts = blogLimit(posts, md.Query("l"))
return r
}
// get data from medium api
resp, err := http.Get(mediumAPIAllPosts)
if err != nil {
md.Err(err)
return Err500
}
// read body and trim the prefix
all, err := ioutil.ReadAll(resp.Body)
if err != nil {
md.Err(err)
return Err500
}
all = bytes.TrimPrefix(all, []byte(mediumAPIResponsePrefix))
// unmarshal into response struct
var mResp mediumResp
err = json.Unmarshal(all, &mResp)
if err != nil {
md.Err(err)
return Err500
}
if !mResp.Success {
md.Err(errors.New("medium api call is not successful"))
return Err500
}
// create posts slice and fill it up with converted posts from the medium
// API
posts := make([]blogPost, len(mResp.Payload.Posts))
for idx, mp := range mResp.Payload.Posts {
var p blogPost
// convert structs
p.ID = mp.ID
p.Title = mp.Title
p.CreatedAt = time.Unix(0, mp.CreatedAt*1000000)
p.UpdatedAt = time.Unix(0, mp.UpdatedAt*1000000)
p.ImportedURL = mp.ImportedURL
p.UniqueSlug = mp.UniqueSlug
cr := mResp.Payload.References.User[mp.CreatorID]
p.Creator.UserID = cr.UserID
p.Creator.Name = cr.Name
p.Creator.Username = cr.Username
p.Snippet = mp.Virtuals.Subtitle
p.WordCount = mp.Virtuals.WordCount
p.ReadingTime = mp.Virtuals.ReadingTime
posts[idx] = p
}
// save in redis
bb := new(bytes.Buffer)
err = gob.NewEncoder(bb).Encode(posts)
if err != nil {
md.Err(err)
return Err500
}
md.R.Set("api:blog_posts", bb.Bytes(), time.Minute*5)
var r blogPostsResponse
r.Code = 200
r.Posts = blogLimit(posts, md.Query("l"))
return r
}
func blogLimit(posts []blogPost, s string) []blogPost {
i := common.Int(s)
if i >= len(posts) || i < 1 {
return posts
}
return posts[:i]
}

465
app/v1/clan.go Normal file
View File

@@ -0,0 +1,465 @@
package v1
import (
"database/sql"
"github.com/osuyozora/api/common"
"sort"
"fmt"
"strconv"
)
type singleClan struct {
ID int `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Tag string `json:"tag"`
Icon string `json:"icon"`
}
type multiClanData struct {
common.ResponseBase
Clans []singleClan `json:"clans"`
}
// clansGET retrieves all the clans on this ripple instance.
func ClansGET(md common.MethodData) common.CodeMessager {
var (
r multiClanData
rows *sql.Rows
err error
)
if md.Query("id") != "" {
rows, err = md.DB.Query("SELECT id, name, description, tag, icon FROM clans WHERE id = ? LIMIT 1", md.Query("id"))
} else {
rows, err = md.DB.Query("SELECT id, name, description, tag, icon FROM clans " + common.Paginate(md.Query("p"), md.Query("l"), 50))
}
if err != nil {
md.Err(err)
return Err500
}
defer rows.Close()
for rows.Next() {
nc := singleClan{}
err = rows.Scan(&nc.ID, &nc.Name, &nc.Description, &nc.Tag, &nc.Icon)
if err != nil {
md.Err(err)
}
r.Clans = append(r.Clans, nc)
}
if err := rows.Err(); err != nil {
md.Err(err)
}
r.ResponseBase.Code = 200
return r
}
type clanMembersData struct {
common.ResponseBase
Members []userNotFullResponseLmao `json:"members"`
}
// get total stats of clan. later.
type totalStats struct {
common.ResponseBase
ClanID int `json:"id"`
ChosenMode modeData `json:"chosen_mode"`
Rank int `json:"rank"`
}
type clanLbSingle struct {
ID int `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Tag string `json:"tag"`
Icon string `json:"icon"`
ChosenMode modeData `json:"chosen_mode"`
Rank int `json:"rank"`
}
type megaStats struct {
common.ResponseBase
Clans []clanLbSingle `json:"clans"`
}
func AllClanStatsGET(md common.MethodData) common.CodeMessager {
var (
r megaStats
rows *sql.Rows
err error
)
rows, err = md.DB.Query("SELECT id, name, description, icon FROM clans")
if err != nil {
fmt.Println("lol")
md.Err(err)
}
defer rows.Close()
for rows.Next() {
nc := clanLbSingle{}
err = rows.Scan(&nc.ID, &nc.Name, &nc.Description, &nc.Icon)
if err != nil {
md.Err(err)
}
nc.ChosenMode.PP = 0
r.Clans = append(r.Clans, nc)
}
if err := rows.Err(); err != nil {
fmt.Println("lol 2")
md.Err(err)
}
r.ResponseBase.Code = 200
// anyone who ever looks into this, yes, i need to kill myself. ~Flame
m, brr := strconv.ParseInt(string(md.Query("m")[19]), 10, 64)
if brr != nil {
fmt.Println("lol 3")
fmt.Println(brr)
m = 0
}
n := "std"
if m == 1 {
n = "taiko"
} else if m == 2 {
n = "ctb"
} else if m == 3 {
n = "mania"
} else {
n = "std"
}
fmt.Println(n)
for i := 0; i < len(r.Clans); i++ {
var members clanMembersData
rid := r.Clans[i].ID
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, users.register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country, users_stats.user_color,
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.pp_std, users_stats.playcount_std, users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.pp_taiko, users_stats.playcount_taiko, users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.pp_ctb, users_stats.playcount_ctb, users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.pp_mania, users_stats.playcount_mania, users_stats.replays_watched_mania, users_stats.total_hits_mania
FROM user_clans uc
INNER JOIN users
ON users.id = uc.user
INNER JOIN users_stats ON users_stats.id = uc.user
WHERE clan = ? AND privileges & 1 = 1
`, rid)
if err != nil {
fmt.Println("lol 4")
fmt.Println(err)
}
members.Code = 200
if n == "std" {
fmt.Printf("%v", r.Clans)
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpStd
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreStd
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreStd
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountStd
}
} else if n == "taiko" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpTaiko
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreTaiko
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreTaiko
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountTaiko
}
} else if n == "ctb" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpCtb
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreCtb
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreCtb
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountCtb
}
} else if n == "mania" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpMania
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreMania
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreMania
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountMania
}
}
r.Clans[i].ChosenMode.PP = (r.Clans[i].ChosenMode.PP / (len(members.Members) + 1))
}
sort.Slice(r.Clans, func(i, j int) bool {
return r.Clans[i].ChosenMode.PP > r.Clans[j].ChosenMode.PP
})
for i := 0; i < len(r.Clans); i++ {
r.Clans[i].Rank = i+1
}
return r
}
func TotalClanStatsGET(md common.MethodData) common.CodeMessager {
var (
r megaStats
rows *sql.Rows
err error
)
rows, err = md.DB.Query("SELECT id, name, description, icon FROM clans")
if err != nil {
md.Err(err)
return Err500
}
defer rows.Close()
for rows.Next() {
nc := clanLbSingle{}
err = rows.Scan(&nc.ID, &nc.Name, &nc.Description, &nc.Icon)
if err != nil {
md.Err(err)
}
nc.ChosenMode.PP = 0
r.Clans = append(r.Clans, nc)
}
if err := rows.Err(); err != nil {
md.Err(err)
}
r.ResponseBase.Code = 200
id := common.Int(md.Query("id"))
if id == 0 {
return ErrMissingField("id")
}
//RETARD CODE HAHAAAA
m, brr := strconv.ParseInt(string(md.Query("m")[11]), 10, 64)
if brr != nil {
fmt.Println(brr)
}
n := "std"
if m == 1 {
n = "taiko"
} else if m == 2 {
n = "ctb"
} else if m == 3 {
n = "mania"
} else {
n = "std"
}
fmt.Println(n)
for i := 0; i < len(r.Clans); i++ {
var members clanMembersData
rid := r.Clans[i].ID
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, users.register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country, users_stats.user_color,
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.pp_std, users_stats.playcount_std, users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.pp_taiko, users_stats.playcount_taiko, users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.pp_ctb, users_stats.playcount_ctb, users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.pp_mania, users_stats.playcount_mania, users_stats.replays_watched_mania, users_stats.total_hits_mania
FROM user_clans uc
INNER JOIN users
ON users.id = uc.user
INNER JOIN users_stats ON users_stats.id = uc.user
WHERE clan = ? AND privileges & 1 = 1
`, rid)
if err != nil {
fmt.Println(err)
}
members.Code = 200
if n == "std" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpStd
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreStd
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreStd
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountStd
r.Clans[i].ChosenMode.ReplaysWatched = r.Clans[i].ChosenMode.ReplaysWatched + members.Members[u].ReplaysWatchedStd
r.Clans[i].ChosenMode.TotalHits = r.Clans[i].ChosenMode.TotalHits + members.Members[u].TotalHitsStd
}
} else if n == "taiko" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpTaiko
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreTaiko
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreTaiko
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountTaiko
r.Clans[i].ChosenMode.ReplaysWatched = r.Clans[i].ChosenMode.ReplaysWatched + members.Members[u].ReplaysWatchedTaiko
r.Clans[i].ChosenMode.TotalHits = r.Clans[i].ChosenMode.TotalHits + members.Members[u].TotalHitsTaiko
}
} else if n == "ctb" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpCtb
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreCtb
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreCtb
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountCtb
r.Clans[i].ChosenMode.ReplaysWatched = r.Clans[i].ChosenMode.ReplaysWatched + members.Members[u].ReplaysWatchedCtb
r.Clans[i].ChosenMode.TotalHits = r.Clans[i].ChosenMode.TotalHits + members.Members[u].TotalHitsStd
}
} else if n == "mania" {
for u := 0; u < len(members.Members); u++ {
r.Clans[i].ChosenMode.PP = r.Clans[i].ChosenMode.PP + members.Members[u].PpMania
r.Clans[i].ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore + members.Members[u].RankedScoreMania
r.Clans[i].ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore + members.Members[u].TotalScoreMania
r.Clans[i].ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount + members.Members[u].PlaycountMania
r.Clans[i].ChosenMode.ReplaysWatched = r.Clans[i].ChosenMode.ReplaysWatched + members.Members[u].ReplaysWatchedMania
r.Clans[i].ChosenMode.TotalHits = r.Clans[i].ChosenMode.TotalHits + members.Members[u].TotalHitsMania
}
}
r.Clans[i].ChosenMode.PP = (r.Clans[i].ChosenMode.PP / (len(members.Members) + 1))
}
sort.Slice(r.Clans, func(i, j int) bool {
return r.Clans[i].ChosenMode.PP > r.Clans[j].ChosenMode.PP
})
for i := 0; i < len(r.Clans); i++ {
r.Clans[i].Rank = i+1
}
b := totalStats{}
for i := 0; i < len(r.Clans); i++ {
if r.Clans[i].ID == id {
b.ClanID = id
b.ChosenMode.PP = r.Clans[i].ChosenMode.PP
b.ChosenMode.RankedScore = r.Clans[i].ChosenMode.RankedScore
b.ChosenMode.TotalScore = r.Clans[i].ChosenMode.TotalScore
b.ChosenMode.PlayCount = r.Clans[i].ChosenMode.PlayCount
b.ChosenMode.ReplaysWatched = r.Clans[i].ChosenMode.ReplaysWatched
b.ChosenMode.TotalHits = r.Clans[i].ChosenMode.TotalHits
b.Rank = r.Clans[i].Rank
b.Code = 200
}
}
return b
}
type isClanData struct {
Clan int `json:"clan"`
User int `json:"user"`
Perms int `json:"perms"`
}
type isClan struct {
common.ResponseBase
Clan isClanData `json:"clan"`
}
func IsInClanGET(md common.MethodData) common.CodeMessager {
ui := md.Query("uid")
if ui == "0" {
return ErrMissingField("uid")
}
var r isClan
rows, err := md.DB.Query("SELECT user, clan, perms FROM user_clans WHERE user = ?", ui)
if err != nil {
md.Err(err)
return Err500
}
defer rows.Close()
for rows.Next() {
nc := isClanData{}
err = rows.Scan(&nc.User, &nc.Clan, &nc.Perms)
if err != nil {
md.Err(err)
}
r.Clan = nc
}
if err := rows.Err(); err != nil {
md.Err(err)
}
r.ResponseBase.Code = 200
return r
}
type imRetarded struct {
common.ResponseBase
Invite string `json:"invite"`
}
func ClanInviteGET(md common.MethodData) common.CodeMessager {
// big perms check lol ok
n := common.Int(md.Query("id"))
var r imRetarded
var clan int
// get user clan, then get invite
md.DB.QueryRow("SELECT clan FROM user_clans WHERE user = ? LIMIT 1", n).Scan(&clan)
row := md.DB.QueryRow("SELECT invite FROM clans_invites WHERE clan = ? LIMIT 1", clan).Scan(&r.Invite)
if row != nil {
fmt.Println(row)
}
return r
}
// ClanMembersGET retrieves the people who are in a certain clan.
func ClanMembersGET(md common.MethodData) common.CodeMessager {
i := common.Int(md.Query("id"))
if i == 0 {
return ErrMissingField("id")
}
r := common.Int(md.Query("r"))
if r == 0 {
var members clanMembersData
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, users.register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country, users_stats.user_color,
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.pp_std, users_stats.playcount_std, users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.pp_taiko, users_stats.playcount_taiko, users_stats.replays_watched_taiko, users_stats.total_hits_taiko
FROM user_clans uc
INNER JOIN users
ON users.id = uc.user
INNER JOIN users_stats ON users_stats.id = uc.user
WHERE clan = ?
ORDER BY id ASC `, i)
if err != nil {
md.Err(err)
return Err500
}
members.Code = 200
return members
} else {
var members clanMembersData
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, users.register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country, users_stats.user_color,
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.pp_std, users_stats.playcount_std, users_stats.replays_watched_std,
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.pp_taiko, users_stats.playcount_taiko, users_stats.replays_watched_taiko
FROM user_clans uc
INNER JOIN users
ON users.id = uc.user
INNER JOIN users_stats ON users_stats.id = uc.user
WHERE clan = ? AND perms = ?
ORDER BY id ASC `, i, r)
if err != nil {
md.Err(err)
return Err500
}
members.Code = 200
return members
}
}

View File

@@ -1,92 +0,0 @@
package v1
import (
"database/sql"
"git.zxq.co/ripple/rippleapi/common"
)
type docFile struct {
ID int `json:"id"`
DocName string `json:"doc_name"`
Public bool `json:"public"`
IsRule bool `json:"is_rule"`
}
type docResponse struct {
common.ResponseBase
Files []docFile `json:"files"`
}
// DocGET retrieves a list of documentation files.
func DocGET(md common.MethodData) common.CodeMessager {
var wc string
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" {
wc = "WHERE public = '1'"
}
rows, err := md.DB.Query("SELECT id, doc_name, public, is_rule FROM docs " + wc)
if err != nil {
md.Err(err)
return Err500
}
var r docResponse
for rows.Next() {
var f docFile
err := rows.Scan(&f.ID, &f.DocName, &f.Public, &f.IsRule)
if err != nil {
md.Err(err)
continue
}
r.Files = append(r.Files, f)
}
r.Code = 200
return r
}
type docContentResponse struct {
common.ResponseBase
Content string `json:"content"`
}
// DocContentGET retrieves the raw markdown file of a doc file
func DocContentGET(md common.MethodData) common.CodeMessager {
docID := common.Int(md.C.Query("id"))
if docID == 0 {
return common.SimpleResponse(404, "Documentation file not found!")
}
var wc string
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" {
wc = "AND public = '1'"
}
var r docContentResponse
err := md.DB.QueryRow("SELECT doc_contents FROM docs WHERE id = ? "+wc+" LIMIT 1", docID).Scan(&r.Content)
switch {
case err == sql.ErrNoRows:
r.Code = 404
r.Message = "Documentation file not found!"
case err != nil:
md.Err(err)
return Err500
default:
r.Code = 200
}
return r
}
// DocRulesGET gets the rules.
func DocRulesGET(md common.MethodData) common.CodeMessager {
var r docContentResponse
err := md.DB.QueryRow("SELECT doc_contents FROM docs WHERE is_rule = '1' LIMIT 1").Scan(&r.Content)
const ruleFree = "# This Ripple instance is rule-free! Yay!"
switch {
case err == sql.ErrNoRows:
r.Content = ruleFree
case err != nil:
md.Err(err)
return Err500
case len(r.Content) == 0:
r.Content = ruleFree
}
r.Code = 200
return r
}

View File

@@ -3,7 +3,7 @@ package v1
import ( import (
"strings" "strings"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
// Boilerplate errors // Boilerplate errors
@@ -16,6 +16,6 @@ var (
func ErrMissingField(missingFields ...string) common.CodeMessager { func ErrMissingField(missingFields ...string) common.CodeMessager {
return common.ResponseBase{ return common.ResponseBase{
Code: 422, // http://stackoverflow.com/a/10323055/5328069 Code: 422, // http://stackoverflow.com/a/10323055/5328069
Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".", Message: "Missing parameters: " + strings.Join(missingFields, ", ") + ".",
} }
} }

View File

@@ -2,9 +2,8 @@ package v1
import ( import (
"database/sql" "database/sql"
"time"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
type friendData struct { type friendData struct {
@@ -40,22 +39,31 @@ func FriendsGET(md common.MethodData) common.CodeMessager {
md.Err(err) md.Err(err)
} }
// Yes.
myFriendsQuery := ` myFriendsQuery := `
SELECT SELECT
users.id, users.username, users.register_datetime, users.rank, users.latest_activity, users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.username_aka,
users_stats.country, users_stats.show_country users_stats.country
FROM users_relationships FROM users_relationships
LEFT JOIN users LEFT JOIN users
ON users_relationships.user2 = users.id ON users_relationships.user2 = users.id
LEFT JOIN users_stats LEFT JOIN users_stats
ON users_relationships.user2=users_stats.id ON users_relationships.user2=users_stats.id
WHERE users_relationships.user1=? WHERE users_relationships.user1=?
ORDER BY users_relationships.id` `
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.C.Query("p"), md.C.Query("l"), 50), md.ID()) myFriendsQuery += common.Sort(md, common.SortConfiguration{
Allowed: []string{
"id",
"username",
"latest_activity",
},
Default: "users.id asc",
Table: "users",
}) + "\n"
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.Query("p"), md.Query("l"), 100), md.ID())
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
@@ -87,23 +95,12 @@ ORDER BY users_relationships.id`
func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) { func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
var err error var err error
registeredOn := int64(0) err = row.Scan(&user.ID, &user.Username, &user.RegisteredOn, &user.Privileges, &user.LatestActivity, &user.UsernameAKA, &user.Country)
latestActivity := int64(0)
var showcountry bool
err = row.Scan(&user.ID, &user.Username, &registeredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showcountry)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return return
} }
user.RegisteredOn = time.Unix(registeredOn, 0)
user.LatestActivity = time.Unix(latestActivity, 0)
// If the user wants to stay anonymous, don't show their country.
// This can be overriden if we have the ReadConfidential privilege and the user we are accessing is the token owner.
if !(showcountry || (md.User.Privileges.HasPrivilegeReadConfidential() && user.ID == md.ID())) {
user.Country = "XX"
}
return return
} }
@@ -117,7 +114,7 @@ type friendsWithResponse struct {
func FriendsWithGET(md common.MethodData) common.CodeMessager { func FriendsWithGET(md common.MethodData) common.CodeMessager {
var r friendsWithResponse var r friendsWithResponse
r.Code = 200 r.Code = 200
uid := common.Int(md.C.Query("id")) uid := common.Int(md.Query("id"))
if uid == 0 { if uid == 0 {
return r return r
} }
@@ -132,9 +129,13 @@ func FriendsWithGET(md common.MethodData) common.CodeMessager {
return r return r
} }
// FriendsAddGET is the GET version of FriendsAddPOST. // FriendsAddPOST adds an user to the friends.
func FriendsAddGET(md common.MethodData) common.CodeMessager { func FriendsAddPOST(md common.MethodData) common.CodeMessager {
return addFriend(md, common.Int(md.C.Query("id"))) var u struct {
User int `json:"user"`
}
md.Unmarshal(&u)
return addFriend(md, u.User)
} }
func addFriend(md common.MethodData, u int) common.CodeMessager { func addFriend(md common.MethodData, u int) common.CodeMessager {
@@ -169,16 +170,21 @@ func addFriend(md common.MethodData, u int) common.CodeMessager {
// userExists makes sure an user exists. // userExists makes sure an user exists.
func userExists(md common.MethodData, u int) (r bool) { func userExists(md common.MethodData, u int) (r bool) {
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND users.allowed='1')", u).Scan(&r) err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND "+
md.User.OnlyUserPublic(true)+")", u).Scan(&r)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
md.Err(err) md.Err(err)
} }
return return
} }
// FriendsDelGET is the GET version of FriendDelPOST. // FriendsDelPOST deletes an user's friend.
func FriendsDelGET(md common.MethodData) common.CodeMessager { func FriendsDelPOST(md common.MethodData) common.CodeMessager {
return delFriend(md, common.Int(md.C.Query("id"))) var u struct {
User int `json:"user"`
}
md.Unmarshal(&u)
return delFriend(md, u.User)
} }
func delFriend(md common.MethodData, u int) common.CodeMessager { func delFriend(md common.MethodData, u int) common.CodeMessager {

View File

@@ -1,5 +0,0 @@
package v1
func init() {
go removeUseless()
}

View File

@@ -2,9 +2,15 @@ package v1
import ( import (
"fmt" "fmt"
"time" "strconv"
"strings"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
redis "gopkg.in/redis.v5"
"zxq.co/ripple/ocl"
"github.com/osuyozora/api/common"
) )
type leaderboardUser struct { type leaderboardUser struct {
@@ -21,58 +27,269 @@ type leaderboardResponse struct {
const lbUserQuery = ` const lbUserQuery = `
SELECT SELECT
users.id, users.username, users.register_datetime, users.rank, users.latest_activity, users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.country, users_stats.show_country, users_stats.username_aka, users_stats.country,
users_stats.play_style, users_stats.favourite_mode, users_stats.play_style, users_stats.favourite_mode,
users_stats.ranked_score_%[1]s, users_stats.total_score_%[1]s, users_stats.playcount_%[1]s, users_stats.ranked_score_%[1]s, users_stats.total_score_%[1]s, users_stats.playcount_%[1]s,
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s, users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s, leaderboard_%[1]s.position as %[1]s_position users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s
FROM leaderboard_%[1]s FROM users
INNER JOIN users ON users.id = leaderboard_%[1]s.user INNER JOIN users_stats ON users_stats.id = users.id
INNER JOIN users_stats ON users_stats.id = leaderboard_%[1]s.user WHERE users.id IN (?)
%[2]s` `
const lbUserQueryRx = `
SELECT
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.country,
users_stats.play_style, users_stats.favourite_mode,
users_stats.ranked_score_%[1]s_rx, users_stats.total_score_%[1]s_rx, users_stats.playcount_%[1]s_rx,
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
users_stats.avg_accuracy_%[1]s_rx, users_stats.pp_%[1]s_rx
FROM users
INNER JOIN users_stats ON users_stats.id = users.id
WHERE users.id IN (?)
`
const lbUserQueryAp = `
SELECT
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.country,
users_stats.play_style, users_stats.favourite_mode,
users_stats.ranked_score_%[1]s_ap, users_stats.total_score_%[1]s_ap, users_stats.playcount_%[1]s_ap,
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
users_stats.avg_accuracy_%[1]s_ap, users_stats.pp_%[1]s_auto
FROM users
INNER JOIN users_stats ON users_stats.id = users.id
WHERE users.id IN (?) AND users.privileges >= 3
`
// LeaderboardGET gets the leaderboard. // LeaderboardGET gets the leaderboard.
func LeaderboardGET(md common.MethodData) common.CodeMessager { func LeaderboardGET(md common.MethodData) common.CodeMessager {
m := getMode(md.C.Query("mode")) m := getMode(md.Query("mode"))
query := fmt.Sprintf(lbUserQuery, m, `WHERE users.allowed = '1' ORDER BY leaderboard_`+m+`.position `+
common.Paginate(md.C.Query("p"), md.C.Query("l"), 100)) // md.Query.Country
rows, err := md.DB.Query(query) p := common.Int(md.Query("p")) - 1
if p < 0 {
p = 0
}
l := common.InString(1, md.Query("l"), 500, 50)
key := "ripple:leaderboard:" + m
if md.Query("country") != "" {
key += ":" + md.Query("country")
}
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
var resp leaderboardResponse
for rows.Next() {
var (
u leaderboardUser
register int64
latestActivity int64
showCountry bool
)
err := rows.Scan(
&u.ID, &u.Username, &register, &u.Rank, &latestActivity,
&u.UsernameAKA, &u.Country, &showCountry, var resp leaderboardResponse
&u.PlayStyle, &u.FavouriteMode, resp.Code = 200
if len(results) == 0 {
return resp
}
query := fmt.Sprintf(lbUserQuery+` ORDER BY users_stats.pp_%[1]s DESC, users_stats.ranked_score_%[1]s DESC`, m)
query, params, _ := sqlx.In(query, results)
rows, err := md.DB.Query(query, params...)
if err != nil {
md.Err(err)
return Err500
}
for rows.Next() {
var u leaderboardUser
err := rows.Scan(
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount, &u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits, &u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
&u.ChosenMode.Accuracy, &u.ChosenMode.PP, &u.ChosenMode.GlobalLeaderboardRank, &u.ChosenMode.Accuracy, &u.ChosenMode.PP,
) )
if err != nil { if err != nil {
md.Err(err) md.Err(err)
continue continue
} }
if !showCountry { u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
u.Country = "XX" if i := leaderboardPosition(md.R, m, u.ID); i != nil {
u.ChosenMode.GlobalLeaderboardRank = i
}
if i := countryPosition(md.R, m, u.ID, u.Country); i != nil {
u.ChosenMode.CountryLeaderboardRank = i
} }
u.RegisteredOn = time.Unix(register, 0)
u.LatestActivity = time.Unix(latestActivity, 0)
resp.Users = append(resp.Users, u) resp.Users = append(resp.Users, u)
} }
resp.Code = 200
return resp return resp
} }
func leaderboardPosition(r *redis.Client, mode string, user int) *int {
return _position(r, "ripple:leaderboard:"+mode, user)
}
func countryPosition(r *redis.Client, mode string, user int, country string) *int {
return _position(r, "ripple:leaderboard:"+mode+":"+strings.ToLower(country), user)
}
func _position(r *redis.Client, key string, user int) *int {
res := r.ZRevRank(key, strconv.Itoa(user))
if res.Err() == redis.Nil {
return nil
}
x := int(res.Val()) + 1
return &x
}
// LeaderboardGET gets the leaderboard.
func LeaderboardRxGET(md common.MethodData) common.CodeMessager {
m := getMode(md.Query("mode"))
// md.Query.Country
p := common.Int(md.Query("p")) - 1
if p < 0 {
p = 0
}
l := common.InString(1, md.Query("l"), 500, 50)
key := "ripple:leaderboard_relax:" + m
if md.Query("country") != "" {
key += ":" + md.Query("country")
}
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
if err != nil {
md.Err(err)
return Err500
}
var resp leaderboardResponse
resp.Code = 200
if len(results) == 0 {
return resp
}
query := fmt.Sprintf(lbUserQueryRx+` ORDER BY users_stats.pp_%[1]s_rx DESC, users_stats.ranked_score_%[1]s_rx DESC`, m)
query, params, _ := sqlx.In(query, results)
rows, err := md.DB.Query(query, params...)
if err != nil {
md.Err(err)
return Err500
}
for rows.Next() {
var u leaderboardUser
err := rows.Scan(
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
&u.ChosenMode.Accuracy, &u.ChosenMode.PP,
)
if err != nil {
md.Err(err)
continue
}
u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
if i := leaderboardPositionRx(md.R, m, u.ID); i != nil {
u.ChosenMode.GlobalLeaderboardRank = i
}
if i := countryPositionRx(md.R, m, u.ID, u.Country); i != nil {
u.ChosenMode.CountryLeaderboardRank = i
}
resp.Users = append(resp.Users, u)
}
return resp
}
// LeaderboardGET gets the leaderboard.
func LeaderboardApGET(md common.MethodData) common.CodeMessager {
m := getMode(md.Query("mode"))
// md.Query.Country
p := common.Int(md.Query("p")) - 1
if p < 0 {
p = 0
}
l := common.InString(1, md.Query("l"), 500, 50)
key := "ripple:leaderboard_auto:" + m
if md.Query("country") != "" {
key += ":" + md.Query("country")
}
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
if err != nil {
md.Err(err)
return Err500
}
var resp leaderboardResponse
resp.Code = 200
if len(results) == 0 {
return resp
}
query := fmt.Sprintf(lbUserQueryAp+` ORDER BY users_stats.pp_%[1]s_auto DESC, users_stats.ranked_score_%[1]s_ap DESC`, m)
query, params, _ := sqlx.In(query, results)
rows, err := md.DB.Query(query, params...)
if err != nil {
md.Err(err)
return Err500
}
for rows.Next() {
var u leaderboardUser
err := rows.Scan(
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
&u.ChosenMode.Accuracy, &u.ChosenMode.PP,
)
if err != nil {
md.Err(err)
continue
}
u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
if i := leaderboardPositionAp(md.R, m, u.ID); i != nil {
u.ChosenMode.GlobalLeaderboardRank = i
}
if i := countryPositionAp(md.R, m, u.ID, u.Country); i != nil {
u.ChosenMode.CountryLeaderboardRank = i
}
resp.Users = append(resp.Users, u)
}
return resp
}
func leaderboardPositionRx(r *redis.Client, mode string, user int) *int {
return _position(r, "ripple:leaderboard_relax:"+mode, user)
}
func countryPositionRx(r *redis.Client, mode string, user int, country string) *int {
return _position(r, "ripple:leaderboard_relax:"+mode+":"+strings.ToLower(country), user)
}
func leaderboardPositionAp(r *redis.Client, mode string, user int) *int {
return _position(r, "ripple:leaderboard_auto:"+mode, user)
}
func countryPositionAp(r *redis.Client, mode string, user int, country string) *int {
return _position(r, "ripple:leaderboard_auto:"+mode+":"+strings.ToLower(country), user)
}

View File

@@ -1,61 +0,0 @@
package v1
import (
"sync"
"time"
)
type failedAttempt struct {
attempt time.Time
ID int
}
var failedAttempts []failedAttempt
var failedAttemptsMutex = new(sync.RWMutex)
// removeUseless removes the expired attempts in failedAttempts
func removeUseless() {
for {
failedAttemptsMutex.RLock()
var localCopy = make([]failedAttempt, len(failedAttempts))
copy(localCopy, failedAttempts)
failedAttemptsMutex.RUnlock()
var newStartFrom int
for k, v := range localCopy {
if time.Since(v.attempt) > time.Minute*10 {
newStartFrom = k + 1
} else {
break
}
}
copySl := localCopy[newStartFrom:]
failedAttemptsMutex.Lock()
failedAttempts = make([]failedAttempt, len(copySl))
for i, v := range copySl {
failedAttempts[i] = v
}
failedAttemptsMutex.Unlock()
time.Sleep(time.Minute * 10)
}
}
func addFailedAttempt(uid int) {
failedAttemptsMutex.Lock()
failedAttempts = append(failedAttempts, failedAttempt{
attempt: time.Now(),
ID: uid,
})
failedAttemptsMutex.Unlock()
}
func nFailedAttempts(uid int) int {
var count int
failedAttemptsMutex.RLock()
for _, i := range failedAttempts {
if i.ID == uid && time.Since(i.attempt) < time.Minute*10 {
count++
}
}
failedAttemptsMutex.RUnlock()
return count
}

View File

@@ -1,9 +1,15 @@
package v1 package v1
import ( import (
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"time" "time"
"git.zxq.co/ripple/rippleapi/common" redis "gopkg.in/redis.v5"
"github.com/osuyozora/api/common"
) )
type setAllowedData struct { type setAllowedData struct {
@@ -13,31 +19,283 @@ type setAllowedData struct {
// UserManageSetAllowedPOST allows to set the allowed status of an user. // UserManageSetAllowedPOST allows to set the allowed status of an user.
func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager { func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
data := setAllowedData{} var data setAllowedData
if err := md.RequestData.Unmarshal(&data); err != nil { if err := md.Unmarshal(&data); err != nil {
return ErrBadJSON return ErrBadJSON
} }
if data.Allowed < 0 || data.Allowed > 2 { if data.Allowed < 0 || data.Allowed > 2 {
return common.SimpleResponse(400, "Allowed status must be between 0 and 2") return common.SimpleResponse(400, "Allowed status must be between 0 and 2")
} }
var banDatetime int64 var banDatetime int64
var privsSet string
if data.Allowed == 0 { if data.Allowed == 0 {
banDatetime = time.Now().Unix() banDatetime = time.Now().Unix()
privsSet = "privileges = (privileges & ~3)"
} else {
banDatetime = 0
privsSet = "privileges = (privileges | 3)"
} }
_, err := md.DB.Exec("UPDATE users SET allowed = ?, ban_datetime = ? WHERE id = ?", data.Allowed, banDatetime, data.UserID) _, err := md.DB.Exec("UPDATE users SET "+privsSet+", ban_datetime = ? WHERE id = ?", banDatetime, data.UserID)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
rapLog(md, fmt.Sprintf("changed UserID:%d's allowed to %d. This was done using the API's terrible ManageSetAllowed.", data.UserID, data.Allowed))
go fixPrivileges(data.UserID, md.DB) go fixPrivileges(data.UserID, md.DB)
query := ` query := `
SELECT users.id, users.username, register_datetime, rank, SELECT users.id, users.username, register_datetime, privileges,
latest_activity, users_stats.username_aka, latest_activity, users_stats.username_aka,
users_stats.country, users_stats.show_country users_stats.country
FROM users FROM users
LEFT JOIN users_stats LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
WHERE users.id=? WHERE users.id=?
LIMIT 1` LIMIT 1`
return userPuts(md, md.DB.QueryRow(query, data.UserID)) return userPutsSingle(md, md.DB.QueryRowx(query, data.UserID))
}
type userEditData struct {
ID int `json:"id"`
Username *string `json:"username"`
UsernameAKA *string `json:"username_aka"`
Privileges *uint64 `json:"privileges"`
Country *string `json:"country"`
SilenceInfo *silenceInfo `json:"silence_info"`
ResetUserpage bool `json:"reset_userpage"`
//ResetAvatar bool `json:"reset_avatar"`
}
var privChangeList = [...]string{
"banned",
"locked",
"restricted",
"removed all restrictions on",
}
// UserEditPOST allows to edit an user's information.
func UserEditPOST(md common.MethodData) common.CodeMessager {
var data userEditData
if err := md.Unmarshal(&data); err != nil {
fmt.Println(err)
return ErrBadJSON
}
if data.ID == 0 {
return common.SimpleResponse(404, "That user could not be found")
}
var prevUser struct {
Username string
Privileges uint64
}
err := md.DB.Get(&prevUser, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", data.ID)
switch err {
case nil: // carry on
case sql.ErrNoRows:
return common.SimpleResponse(404, "That user could not be found")
default:
md.Err(err)
return Err500
}
const initQuery = "UPDATE users SET\n"
q := initQuery
var args []interface{}
// totally did not realise I had to update some fields in users_stats as well
// and just copy pasting the above code by prefixing "stats" to every
// variable
const statsInitQuery = "UPDATE users_stats SET\n"
statsQ := statsInitQuery
var statsArgs []interface{}
if common.UserPrivileges(prevUser.Privileges)&common.AdminPrivilegeManageUsers != 0 &&
data.ID != md.User.UserID {
return common.SimpleResponse(403, "Can't edit that user")
}
var isBanned bool
if data.Privileges != nil {
// If we want to modify privileges other than Normal/Public, we need to have
// the right privilege ourselves and AdminManageUsers won't suffice.
if (*data.Privileges&^3) != (prevUser.Privileges&^3) &&
md.User.UserPrivileges&common.AdminPrivilegeManagePrivilege == 0 {
return common.SimpleResponse(403, "Can't modify user privileges without AdminManagePrivileges")
}
q += "privileges = ?,\n"
args = append(args, *data.Privileges)
// UserPublic became 0, so banned or restricted
const uPublic = uint64(common.UserPrivilegePublic)
if *data.Privileges&uPublic == 0 && prevUser.Privileges&uPublic != 0 {
q += "ban_datetime = ?,\n"
args = append(args, time.Now().Unix())
isBanned = true
}
// If we modified other privileges apart from Normal and Public, we use a generic
// "changed user's privileges". Otherwise, we are more descriptive.
if *data.Privileges^prevUser.Privileges > 3 {
rapLog(md, fmt.Sprintf("has changed %s's privileges", prevUser.Username))
} else {
rapLog(md, fmt.Sprintf("has %s %s", privChangeList[*data.Privileges&3], prevUser.Username))
}
}
if data.Username != nil {
if strings.Contains(*data.Username, " ") && strings.Contains(*data.Username, "_") {
return common.SimpleResponse(400, "Mixed spaces and underscores")
}
if usernameAvailable(md, *data.Username, data.ID) {
return common.SimpleResponse(409, "User with that username exists")
}
jsonData, _ := json.Marshal(struct {
UserID int `json:"userID"`
NewUsername string `json:"newUsername"`
}{data.ID, *data.Username})
md.R.Publish("peppy:change_username", string(jsonData))
}
if data.UsernameAKA != nil {
statsQ += "username_aka = ?,\n"
statsArgs = append(statsArgs, *data.UsernameAKA)
}
if data.Country != nil {
statsQ += "country = ?,\n"
statsArgs = append(statsArgs, *data.Country)
rapLog(md, fmt.Sprintf("has changed %s country to %s", prevUser.Username, *data.Country))
appendToUserNotes(md, "country changed to "+*data.Country, data.ID)
}
if data.SilenceInfo != nil && md.User.UserPrivileges&common.AdminPrivilegeSilenceUsers != 0 {
q += "silence_end = ?, silence_reason = ?,\n"
args = append(args, time.Time(data.SilenceInfo.End).Unix(), data.SilenceInfo.Reason)
}
if data.ResetUserpage {
statsQ += "userpage_content = '',\n"
}
if q != initQuery {
q = q[:len(q)-2] + " WHERE id = ? LIMIT 1"
args = append(args, data.ID)
_, err = md.DB.Exec(q, args...)
if err != nil {
md.Err(err)
return Err500
}
}
if statsQ != statsInitQuery {
statsQ = statsQ[:len(statsQ)-2] + " WHERE id = ? LIMIT 1"
statsArgs = append(statsArgs, data.ID)
_, err = md.DB.Exec(statsQ, statsArgs...)
if err != nil {
md.Err(err)
return Err500
}
}
if isBanned {
if err := updateBanBancho(md.R, data.ID); err != nil {
md.Err(err)
return Err500
}
}
rapLog(md, fmt.Sprintf("has updated user %s", prevUser.Username))
return userPutsSingle(md, md.DB.QueryRowx(userFields+" WHERE users.id = ? LIMIT 1", data.ID))
}
func updateBanBancho(r *redis.Client, user int) error {
return r.Publish("peppy:ban", strconv.Itoa(user)).Err()
}
type wipeUserData struct {
ID int `json:"id"`
Modes []int `json:"modes"`
}
// WipeUserPOST wipes an user's scores.
func WipeUserPOST(md common.MethodData) common.CodeMessager {
var data wipeUserData
if err := md.Unmarshal(&data); err != nil {
return ErrBadJSON
}
if data.ID == 0 {
return ErrMissingField("id")
}
if len(data.Modes) == 0 {
return ErrMissingField("modes")
}
var userData struct {
Username string
Privileges uint64
}
err := md.DB.Get(&userData, "SELECT username, privileges FROM users WHERE id = ?", data.ID)
switch err {
case sql.ErrNoRows:
return common.SimpleResponse(404, "That user could not be found!")
case nil: // carry on
default:
md.Err(err)
return Err500
}
if common.UserPrivileges(userData.Privileges)&common.AdminPrivilegeManageUsers != 0 {
return common.SimpleResponse(403, "Can't edit that user")
}
tx, err := md.DB.Beginx()
if err != nil {
md.Err(err)
return Err500
}
for _, mode := range data.Modes {
if mode < 0 || mode > 3 {
continue
}
_, err = tx.Exec("INSERT INTO scores_removed SELECT * FROM scores WHERE userid = ? AND play_mode = ?", data.ID, mode)
if err != nil {
md.Err(err)
}
_, err = tx.Exec("DELETE FROM scores WHERE userid = ? AND play_mode = ?", data.ID, mode)
if err != nil {
md.Err(err)
}
_, err = tx.Exec(strings.Replace(
`UPDATE users_stats SET total_score_MODE = 0, ranked_score_MODE = 0, replays_watched_MODE = 0,
playcount_MODE = 0, avg_accuracy_MODE = 0, total_hits_MODE = 0, level_MODE = 0, pp_MODE = 0
WHERE id = ?`, "MODE", modesToReadable[mode], -1,
), data.ID)
if err != nil {
md.Err(err)
}
}
if err = tx.Commit(); err != nil {
md.Err(err)
return Err500
}
rapLog(md, fmt.Sprintf("has wiped %s's account", userData.Username))
return userPutsSingle(md, md.DB.QueryRowx(userFields+" WHERE users.id = ? LIMIT 1", data.ID))
}
func appendToUserNotes(md common.MethodData, message string, user int) {
message = "\n[" + time.Now().Format("2006-01-02 15:04:05") + "] API: " + message
_, err := md.DB.Exec("UPDATE users SET notes = CONCAT(COALESCE(notes, ''), ?) WHERE id = ?",
message, user)
if err != nil {
md.Err(err)
}
}
func usernameAvailable(md common.MethodData, u string, userID int) (r bool) {
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE username_safe = ? AND id != ?)", common.SafeUsername(u), userID).Scan(&r)
if err != nil && err != sql.ErrNoRows {
md.Err(err)
}
return
} }

View File

@@ -1,7 +1,10 @@
// +build !windows
// TODO: Make all these methods POST
package v1 package v1
import ( import (
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@@ -10,7 +13,7 @@ import (
"syscall" "syscall"
"time" "time"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
// MetaRestartGET restarts the API with Zero Downtime™. // MetaRestartGET restarts the API with Zero Downtime™.
@@ -26,26 +29,6 @@ func MetaRestartGET(md common.MethodData) common.CodeMessager {
return common.SimpleResponse(200, "brb") return common.SimpleResponse(200, "brb")
} }
// MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD.
// Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time.
func MetaKillGET(md common.MethodData) common.CodeMessager {
proc, err := os.FindProcess(syscall.Getpid())
if err != nil {
return common.SimpleResponse(500, "couldn't find process. what the fuck?")
}
const form = "02/01/2006"
r := common.ResponseBase{
Code: 200,
Message: fmt.Sprintf("RIP ripple API %s - %s", upSince.Format(form), time.Now().Format(form)),
}
// yes
go func() {
time.Sleep(time.Second)
proc.Kill()
}()
return r
}
var upSince = time.Now() var upSince = time.Now()
type metaUpSinceResponse struct { type metaUpSinceResponse struct {
@@ -78,7 +61,7 @@ func MetaUpdateGET(md common.MethodData) common.CodeMessager {
if !execCommand("go", "get", "-v", "-u", "-d") { if !execCommand("go", "get", "-v", "-u", "-d") {
return return
} }
if !execCommand("go", "build", "-v", "-o", "api") { if !execCommand("bash", "-c", "go build -v -ldflags \"-X main.Version=`git rev-parse HEAD`\"") {
return return
} }

42
app/v1/meta_windows.go Normal file
View File

@@ -0,0 +1,42 @@
// +build windows
package v1
import (
"time"
"github.com/osuyozora/api/common"
)
// MetaRestartGET restarts the API with Zero Downtime™.
func MetaRestartGET(md common.MethodData) common.CodeMessager {
return common.SimpleResponse(200, "brb in your dreams")
}
// MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD.
// Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time.
func MetaKillGET(md common.MethodData) common.CodeMessager {
return common.SimpleResponse(200, "haha")
}
var upSince = time.Now()
type metaUpSinceResponse struct {
common.ResponseBase
Code int `json:"code"`
Since int64 `json:"since"`
}
// MetaUpSinceGET retrieves the moment the API application was started.
// Mainly used to get if the API was restarted.
func MetaUpSinceGET(md common.MethodData) common.CodeMessager {
return metaUpSinceResponse{
Code: 200,
Since: int64(upSince.UnixNano()),
}
}
// MetaUpdateGET updates the API to the latest version, and restarts it.
func MetaUpdateGET(md common.MethodData) common.CodeMessager {
return common.SimpleResponse(200, "lol u wish")
}

View File

@@ -4,7 +4,7 @@ import (
"math/rand" "math/rand"
"time" "time"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/common"
) )
var rn = rand.New(rand.NewSource(time.Now().UnixNano())) var rn = rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -79,16 +79,26 @@ var randomSentences = [...]string{
"Superman dies", "Superman dies",
"PP when?", "PP when?",
"RWC hype", "RWC hype",
"I'd just like to interject for a moment.",
"Running on an apple pie!",
":thinking:",
"The total entropy of an isolated system can only increase over time",
"Where are my testicles, Summer?",
"Why don't you ask the smartest people in the universe? Oh yeah, you can't. They blew up.",
} }
func surpriseMe() string { func surpriseMe() string {
return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))] n := int(time.Now().UnixNano())
return randomSentences[n%len(randomSentences)] + " " + kaomojis[n%len(kaomojis)]
} }
type pingResponse struct { type pingResponse struct {
common.ResponseBase common.ResponseBase
ID int `json:"user_id"` ID int `json:"user_id"`
Privileges int `json:"privileges"` Privileges common.Privileges `json:"privileges"`
UserPrivileges common.UserPrivileges `json:"user_privileges"`
PrivilegesS string `json:"privileges_string"`
UserPrivilegesS string `json:"user_privileges_string"`
} }
// PingGET is a message to check with the API that we are logged in, and know what are our privileges. // PingGET is a message to check with the API that we are logged in, and know what are our privileges.
@@ -97,13 +107,16 @@ func PingGET(md common.MethodData) common.CodeMessager {
r.Code = 200 r.Code = 200
if md.ID() == 0 { if md.ID() == 0 {
r.Message = "You have not given us a token, so we don't know who you are! But you can still login with /api/v1/tokens/new " + kaomojis[rn.Intn(len(kaomojis))] r.Message = "You have not given us a token, so we don't know who you are! But you can still login with POST /tokens " + kaomojis[rn.Intn(len(kaomojis))]
} else { } else {
r.Message = surpriseMe() r.Message = surpriseMe()
} }
r.ID = md.ID() r.ID = md.ID()
r.Privileges = int(md.User.Privileges) r.Privileges = md.User.TokenPrivileges
r.UserPrivileges = md.User.UserPrivileges
r.PrivilegesS = md.User.TokenPrivileges.String()
r.UserPrivilegesS = md.User.UserPrivileges.String()
return r return r
} }

View File

@@ -1,43 +0,0 @@
package v1
import (
"git.zxq.co/ripple/rippleapi/common"
)
type privilegesData struct {
common.ResponseBase
Read bool `json:"read"`
ReadConfidential bool `json:"read_confidential"`
Write bool `json:"write"`
ManageBadges bool `json:"manage_badges"`
BetaKeys bool `json:"beta_keys"`
ManageSettings bool `json:"manage_settings"`
ViewUserAdvanced bool `json:"view_user_advanced"`
ManageUser bool `json:"manage_user"`
ManageRoles bool `json:"manage_roles"`
ManageAPIKeys bool `json:"manage_api_keys"`
Blog bool `json:"blog"`
APIMeta bool `json:"api_meta"`
Beatmap bool `json:"beatmap"`
}
// PrivilegesGET returns an explaination for the privileges, telling the client what they can do with this token.
func PrivilegesGET(md common.MethodData) common.CodeMessager {
r := privilegesData{}
r.Code = 200
// This code sucks.
r.Read = md.User.Privileges.HasPrivilegeRead()
r.ReadConfidential = md.User.Privileges.HasPrivilegeReadConfidential()
r.Write = md.User.Privileges.HasPrivilegeWrite()
r.ManageBadges = md.User.Privileges.HasPrivilegeManageBadges()
r.BetaKeys = md.User.Privileges.HasPrivilegeBetaKeys()
r.ManageSettings = md.User.Privileges.HasPrivilegeManageSettings()
r.ViewUserAdvanced = md.User.Privileges.HasPrivilegeViewUserAdvanced()
r.ManageUser = md.User.Privileges.HasPrivilegeManageUser()
r.ManageRoles = md.User.Privileges.HasPrivilegeManageRoles()
r.ManageAPIKeys = md.User.Privileges.HasPrivilegeManageAPIKeys()
r.Blog = md.User.Privileges.HasPrivilegeBlog()
r.APIMeta = md.User.Privileges.HasPrivilegeAPIMeta()
r.Beatmap = md.User.Privileges.HasPrivilegeBeatmap()
return r
}

87
app/v1/rap.go Normal file
View File

@@ -0,0 +1,87 @@
package v1
import (
"fmt"
"time"
"github.com/osuyozora/api/common"
)
type rapLogData struct {
Through string `json:"through"`
Text string `json:"text"`
}
type rapLogMessage struct {
rapLogData
Author int `json:"author"`
CreatedAt time.Time `json:"created_at"`
}
type rapLogResponse struct {
common.ResponseBase
rapLogMessage
}
// RAPLogPOST creates a new entry in the RAP logs
func RAPLogPOST(md common.MethodData) common.CodeMessager {
if md.User.UserPrivileges&common.AdminPrivilegeAccessRAP == 0 {
return common.SimpleResponse(403, "Got lost, kiddo?")
}
var d rapLogData
if err := md.Unmarshal(&d); err != nil {
fmt.Println(err)
return ErrBadJSON
}
if d.Text == "" {
return ErrMissingField("text")
}
if d.Through == "" {
ua := string(md.Ctx.UserAgent())
if len(ua) > 20 {
ua = ua[:20] + "…"
}
d.Through = "API"
if ua != "" {
d.Through += " (" + ua + ")"
}
}
if len(d.Through) > 30 {
d.Through = d.Through[:30]
}
created := time.Now()
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
md.User.UserID, d.Text, created.Unix(), d.Through)
if err != nil {
md.Err(err)
return Err500
}
var resp rapLogResponse
resp.rapLogData = d
resp.Author = md.User.UserID
resp.CreatedAt = created.Truncate(time.Second)
resp.Code = 200
return resp
}
func rapLog(md common.MethodData, message string) {
ua := string(md.Ctx.UserAgent())
if len(ua) > 20 {
ua = ua[:20] + "…"
}
through := "API"
if ua != "" {
through += " (" + ua + ")"
}
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
md.User.UserID, message, time.Now().Unix(), through)
if err != nil {
md.Err(err)
}
}

291
app/v1/score.go Normal file
View File

@@ -0,0 +1,291 @@
package v1
import (
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
)
// Score is a score done on Ripple.
type Score struct {
ID int `json:"id"`
BeatmapMD5 string `json:"beatmap_md5"`
Score int64 `json:"score"`
MaxCombo int `json:"max_combo"`
FullCombo bool `json:"full_combo"`
Mods int `json:"mods"`
Count300 int `json:"count_300"`
Count100 int `json:"count_100"`
Count50 int `json:"count_50"`
CountGeki int `json:"count_geki"`
CountKatu int `json:"count_katu"`
CountMiss int `json:"count_miss"`
Time common.UnixTimestamp `json:"time"`
PlayMode int `json:"play_mode"`
Accuracy float64 `json:"accuracy"`
PP float32 `json:"pp"`
Rank string `json:"rank"`
Completed int `json:"completed"`
}
// beatmapScore is to differentiate from userScore, as beatmapScore contains
// also an user, while userScore contains the beatmap.
type beatmapScore struct {
Score
User userData `json:"user"`
}
type scoresResponse struct {
common.ResponseBase
Scores []beatmapScore `json:"scores"`
}
// ScoresGET retrieves the top scores for a certain beatmap.
func ScoresGET(md common.MethodData) common.CodeMessager {
var (
where = new(common.WhereClause)
r scoresResponse
)
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
switch {
case md.Query("md5") != "":
where.In("beatmap_md5", pm("md5")...)
case md.Query("b") != "":
var md5 string
err := md.DB.Get(&md5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", md.Query("b"))
switch {
case err == sql.ErrNoRows:
r.Code = 200
return r
case err != nil:
md.Err(err)
return Err500
}
where.Where("beatmap_md5 = ?", md5)
}
where.In("scores.id", pm("id")...)
sort := common.Sort(md, common.SortConfiguration{
Default: "scores.pp DESC, scores.score DESC",
Table: "scores",
Allowed: []string{"pp", "score", "accuracy", "id"},
})
if where.Clause == "" {
return ErrMissingField("must specify at least one queried item")
}
where.Where(` scores.completed = '3' AND `+md.User.OnlyUserPublic(false)+` `+
genModeClause(md)+` `+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), "FIF")
where.Params = where.Params[:len(where.Params)-1]
rows, err := md.DB.Query(`
SELECT
scores.id, scores.beatmap_md5, scores.score,
scores.max_combo, scores.full_combo, scores.mods,
scores.300_count, scores.100_count, scores.50_count,
scores.gekis_count, scores.katus_count, scores.misses_count,
scores.time, scores.play_mode, scores.accuracy, scores.pp,
scores.completed,
users.id, users.username, users.register_datetime, users.privileges,
users.latest_activity, users_stats.username_aka, users_stats.country
FROM scores
INNER JOIN users ON users.id = scores.userid
INNER JOIN users_stats ON users_stats.id = scores.userid
`+where.Clause, where.Params...)
if err != nil {
md.Err(err)
return Err500
}
for rows.Next() {
var (
s beatmapScore
u userData
)
err := rows.Scan(
&s.ID, &s.BeatmapMD5, &s.Score.Score,
&s.MaxCombo, &s.FullCombo, &s.Mods,
&s.Count300, &s.Count100, &s.Count50,
&s.CountGeki, &s.CountKatu, &s.CountMiss,
&s.Time, &s.PlayMode, &s.Accuracy, &s.PP,
&s.Completed,
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges,
&u.LatestActivity, &u.UsernameAKA, &u.Country,
)
if err != nil {
md.Err(err)
continue
}
s.User = u
s.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(s.PlayMode),
osuapi.Mods(s.Mods),
s.Accuracy,
s.Count300,
s.Count100,
s.Count50,
s.CountMiss,
))
r.Scores = append(r.Scores, s)
}
r.Code = 200
return r
}
type scoreReportData struct {
ScoreID int `json:"score_id"`
Data json.RawMessage `json:"data"`
Anticheat string `json:"anticheat"`
Severity float32 `json:"severity"`
}
type scoreReport struct {
ID int `json:"id"`
scoreReportData
}
type scoreReportResponse struct {
common.ResponseBase
scoreReport
}
// ScoreReportPOST creates a new report for a score
func ScoreReportPOST(md common.MethodData) common.CodeMessager {
var data scoreReportData
err := md.Unmarshal(&data)
if err != nil {
return ErrBadJSON
}
// Check if there are any missing fields
var missingFields []string
if data.ScoreID == 0 {
missingFields = append(missingFields, "score_id")
}
if data.Anticheat == "" {
missingFields = append(missingFields, "anticheat")
}
if len(missingFields) > 0 {
return ErrMissingField(missingFields...)
}
tx, err := md.DB.Beginx()
if err != nil {
md.Err(err)
return Err500
}
// Get anticheat ID
var id int
err = tx.Get(&id, "SELECT id FROM anticheats WHERE name = ? LIMIT 1", data.Anticheat)
switch err {
case nil: // carry on
case sql.ErrNoRows:
// Create anticheat!
res, err := tx.Exec("INSERT INTO anticheats (name) VALUES (?);", data.Anticheat)
if err != nil {
md.Err(err)
return Err500
}
lid, err := res.LastInsertId()
if err != nil {
md.Err(err)
return Err500
}
id = int(lid)
default:
md.Err(err)
return Err500
}
d := sql.NullString{String: string(data.Data), Valid: true}
if d.String == "null" || d.String == `""` ||
d.String == "[]" || d.String == "{}" || d.String == "0" {
d.Valid = false
}
res, err := tx.Exec("INSERT INTO anticheat_reports (score_id, anticheat_id, data, severity) VALUES (?, ?, ?, ?)",
data.ScoreID, id, d, data.Severity)
if err != nil {
md.Err(err)
return Err500
}
lid, err := res.LastInsertId()
if err != nil {
md.Err(err)
return Err500
}
err = tx.Commit()
if err != nil {
md.Err(err)
return Err500
}
if !d.Valid {
data.Data = json.RawMessage("null")
}
repData := scoreReportResponse{
scoreReport: scoreReport{
ID: int(lid),
scoreReportData: data,
},
}
repData.Code = 200
return repData
}
func getMode(m string) string {
switch m {
case "1":
return "taiko"
case "2":
return "ctb"
case "3":
return "mania"
default:
return "std"
}
}
func genModeClause(md common.MethodData) string {
var modeClause string
if md.Query("mode") != "" {
m, err := strconv.Atoi(md.Query("mode"))
if err == nil && m >= 0 && m <= 3 {
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
}
}
return modeClause
}
func genModeClauseRx(md common.MethodData) string {
var modeClause string
if md.Query("mode") != "" {
m, err := strconv.Atoi(md.Query("mode"))
if err == nil && m >= 0 && m <= 3 {
modeClause = fmt.Sprintf("AND scores_relax.play_mode = '%d'", m)
}
}
return modeClause
}
func genModeClauseAp(md common.MethodData) string {
var modeClause string
if md.Query("mode") != "" {
m, err := strconv.Atoi(md.Query("mode"))
if err == nil && m >= 0 && m <= 3 {
modeClause = fmt.Sprintf("AND scores_auto.play_mode = '%d'", m)
}
}
return modeClause
}

167
app/v1/self.go Normal file
View File

@@ -0,0 +1,167 @@
package v1
import (
"strings"
"github.com/osuyozora/api/common"
"zxq.co/ripple/semantic-icons-ugc"
)
type donorInfoResponse struct {
common.ResponseBase
HasDonor bool `json:"has_donor"`
Expiration common.UnixTimestamp `json:"expiration"`
}
// UsersSelfDonorInfoGET returns information about the users' donor status
func UsersSelfDonorInfoGET(md common.MethodData) common.CodeMessager {
var r donorInfoResponse
var privileges uint64
err := md.DB.QueryRow("SELECT privileges, donor_expire FROM users WHERE id = ?", md.ID()).
Scan(&privileges, &r.Expiration)
if err != nil {
md.Err(err)
return Err500
}
r.HasDonor = common.UserPrivileges(privileges)&common.UserPrivilegeDonor > 0
r.Code = 200
return r
}
type favouriteModeResponse struct {
common.ResponseBase
FavouriteMode int `json:"favourite_mode"`
}
// UsersSelfFavouriteModeGET gets the current user's favourite mode
func UsersSelfFavouriteModeGET(md common.MethodData) common.CodeMessager {
var f favouriteModeResponse
f.Code = 200
if md.ID() == 0 {
return f
}
err := md.DB.QueryRow("SELECT users_stats.favourite_mode FROM users_stats WHERE id = ?", md.ID()).
Scan(&f.FavouriteMode)
if err != nil {
md.Err(err)
return Err500
}
return f
}
type userSettingsData struct {
UsernameAKA *string `json:"username_aka"`
FavouriteMode *int `json:"favourite_mode"`
CustomBadge struct {
singleBadge
Show *bool `json:"show"`
} `json:"custom_badge"`
PlayStyle *int `json:"play_style"`
}
// UsersSelfSettingsPOST allows to modify information about the current user.
func UsersSelfSettingsPOST(md common.MethodData) common.CodeMessager {
var d userSettingsData
md.Unmarshal(&d)
// input sanitisation
*d.UsernameAKA = common.SanitiseString(*d.UsernameAKA)
if md.User.UserPrivileges&common.UserPrivilegeDonor > 0 {
d.CustomBadge.Name = common.SanitiseString(d.CustomBadge.Name)
d.CustomBadge.Icon = sanitiseIconName(d.CustomBadge.Icon)
} else {
d.CustomBadge.singleBadge = singleBadge{}
d.CustomBadge.Show = nil
}
d.FavouriteMode = intPtrIn(0, d.FavouriteMode, 3)
q := new(common.UpdateQuery).
Add("s.username_aka", d.UsernameAKA).
Add("s.favourite_mode", d.FavouriteMode).
Add("s.custom_badge_name", d.CustomBadge.Name).
Add("s.custom_badge_icon", d.CustomBadge.Icon).
Add("s.show_custom_badge", d.CustomBadge.Show).
Add("s.play_style", d.PlayStyle)
_, err := md.DB.Exec("UPDATE users u, users_stats s SET "+q.Fields()+" WHERE s.id = u.id AND u.id = ?", append(q.Parameters, md.ID())...)
if err != nil {
md.Err(err)
return Err500
}
return UsersSelfSettingsGET(md)
}
func sanitiseIconName(s string) string {
classes := strings.Split(s, " ")
n := make([]string, 0, len(classes))
for _, c := range classes {
if !in(c, n) && in(c, semanticiconsugc.SaneIcons) {
n = append(n, c)
}
}
return strings.Join(n, " ")
}
func in(a string, b []string) bool {
for _, x := range b {
if x == a {
return true
}
}
return false
}
type userSettingsResponse struct {
common.ResponseBase
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Flags uint `json:"flags"`
userSettingsData
}
// UsersSelfSettingsGET allows to get "sensitive" information about the current user.
func UsersSelfSettingsGET(md common.MethodData) common.CodeMessager {
var r userSettingsResponse
var ccb bool
r.Code = 200
err := md.DB.QueryRow(`
SELECT
u.id, u.username,
u.email, s.username_aka, s.favourite_mode,
s.show_custom_badge, s.custom_badge_icon,
s.custom_badge_name, s.can_custom_badge,
s.play_style, u.flags
FROM users u
LEFT JOIN users_stats s ON u.id = s.id
WHERE u.id = ?`, md.ID()).Scan(
&r.ID, &r.Username,
&r.Email, &r.UsernameAKA, &r.FavouriteMode,
&r.CustomBadge.Show, &r.CustomBadge.Icon,
&r.CustomBadge.Name, &ccb,
&r.PlayStyle, &r.Flags,
)
if err != nil {
md.Err(err)
return Err500
}
if !ccb {
r.CustomBadge = struct {
singleBadge
Show *bool `json:"show"`
}{}
}
return r
}
func intPtrIn(x int, y *int, z int) *int {
if y == nil {
return nil
}
if *y > z {
return nil
}
if *y < x {
return nil
}
return y
}

View File

@@ -2,136 +2,32 @@ package v1
import ( import (
"crypto/md5" "crypto/md5"
"database/sql" "crypto/sha256"
"encoding/json"
"errors"
"fmt" "fmt"
"strconv"
"time"
"git.zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx"
"git.zxq.co/ripple/schiavolib"
"golang.org/x/crypto/bcrypt" "github.com/osuyozora/api/common"
"zxq.co/ripple/schiavolib"
) )
type tokenNewInData struct { // TokenSelfDeletePOST deletes the token the user is connecting with.
// either username or userid must be given in the request. func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
// if none is given, the request is trashed.
Username string `json:"username"`
UserID int `json:"id"`
Password string `json:"password"`
Privileges int `json:"privileges"`
Description string `json:"description"`
}
type tokenNewResponse struct {
common.ResponseBase
Username string `json:"username"`
ID int `json:"id"`
Privileges int `json:"privileges"`
Token string `json:"token,omitempty"`
Banned bool `json:"banned"`
}
// TokenNewPOST is the handler for POST /token/new.
func TokenNewPOST(md common.MethodData) common.CodeMessager {
var r tokenNewResponse
data := tokenNewInData{}
err := md.RequestData.Unmarshal(&data)
if err != nil {
return ErrBadJSON
}
var miss []string
if data.Username == "" && data.UserID == 0 {
miss = append(miss, "username|id")
}
if data.Password == "" {
miss = append(miss, "password")
}
if len(miss) != 0 {
return ErrMissingField(miss...)
}
var q *sql.Row
const base = "SELECT id, username, rank, password_md5, password_version, allowed FROM users "
if data.UserID != 0 {
q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID)
} else {
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username)
}
var (
rank int
pw string
pwVersion int
allowed int
)
err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &allowed)
switch {
case err == sql.ErrNoRows:
return common.SimpleResponse(404, "No user with that username/id was found.")
case err != nil:
md.Err(err)
return Err500
}
if nFailedAttempts(r.ID) > 20 {
return common.SimpleResponse(429, "You've made too many login attempts. Try again later.")
}
if pwVersion == 1 {
return common.SimpleResponse(418, "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website.")
}
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
go addFailedAttempt(r.ID)
return common.SimpleResponse(403, "That password doesn't match!")
}
md.Err(err)
return Err500
}
if allowed == 0 {
r.Code = 200
r.Message = "That user is banned."
r.Banned = true
return r
}
r.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank))
var (
tokenStr string
tokenMD5 string
)
for {
tokenStr = common.RandomString(32)
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
r.Token = tokenStr
id := 0
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=? LIMIT 1", tokenMD5).Scan(&id)
if err == sql.ErrNoRows {
break
}
if err != nil {
md.Err(err)
return Err500
}
}
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token, private) VALUES (?, ?, ?, ?, '0')", r.ID, r.Privileges, data.Description, tokenMD5)
if err != nil {
md.Err(err)
return Err500
}
r.Code = 200
return r
}
// TokenSelfDeleteGET deletes the token the user is connecting with.
func TokenSelfDeleteGET(md common.MethodData) common.CodeMessager {
if md.ID() == 0 { if md.ID() == 0 {
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!") return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
} }
_, err := md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1", var err error
if md.IsBearer() {
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
} else {
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value)))) fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
}
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
@@ -143,22 +39,29 @@ type token struct {
ID int `json:"id"` ID int `json:"id"`
Privileges uint64 `json:"privileges"` Privileges uint64 `json:"privileges"`
Description string `json:"description"` Description string `json:"description"`
LastUpdated common.UnixTimestamp `json:"last_updated"`
} }
type tokenResponse struct { type tokenResponse struct {
common.ResponseBase common.ResponseBase
Tokens []token `json:"token"` Tokens []token `json:"tokens"`
} }
// TokenGET retrieves a list listing all the user's public tokens. // TokenGET retrieves a list listing all the user's public tokens.
func TokenGET(md common.MethodData) common.CodeMessager { func TokenGET(md common.MethodData) common.CodeMessager {
rows, err := md.DB.Query("SELECT id, privileges, description FROM tokens WHERE user = ? AND private = '0'", md.ID()) wc := common.Where("user = ? AND private = 0", strconv.Itoa(md.ID()))
if md.Query("id") != "" {
wc.Where("id = ?", md.Query("id"))
}
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens "+
wc.Clause+common.Paginate(md.Query("p"), md.Query("l"), 50), wc.Params...)
if err != nil { if err != nil {
return Err500 return Err500
} }
var r tokenResponse var r tokenResponse
for rows.Next() { for rows.Next() {
var t token var t token
err = rows.Scan(&t.ID, &t.Privileges, &t.Description) err = rows.Scan(&t.ID, &t.Privileges, &t.Description, &t.LastUpdated)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
continue continue
@@ -169,17 +72,67 @@ func TokenGET(md common.MethodData) common.CodeMessager {
return r return r
} }
type oauthClient struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID int `json:"owner_id"`
Avatar string `json:"avatar"`
}
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
func (o *oauthClient) Scan(src interface{}) error {
var s []byte
switch x := src.(type) {
case string:
s = []byte(x)
case []byte:
s = x
default:
return errors.New("Can't scan non-string")
}
var vals [3]string
err := json.Unmarshal(s, &vals)
if err != nil {
return err
}
o.Name = vals[0]
o.OwnerID, _ = strconv.Atoi(vals[1])
o.Avatar = vals[2]
return nil
}
type bearerToken struct {
Client oauthClient `json:"client"`
Scope string `json:"scope"`
Privileges common.Privileges `json:"privileges"`
Created time.Time `json:"created"`
}
type tokenSingleResponse struct { type tokenSingleResponse struct {
common.ResponseBase common.ResponseBase
token token
} }
type bearerTokenSingleResponse struct {
common.ResponseBase
bearerToken
}
// TokenSelfGET retrieves information about the token the user is connecting with. // TokenSelfGET retrieves information about the token the user is connecting with.
func TokenSelfGET(md common.MethodData) common.CodeMessager { func TokenSelfGET(md common.MethodData) common.CodeMessager {
if md.ID() == 0 {
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
}
if md.IsBearer() {
return getBearerToken(md)
}
var r tokenSingleResponse var r tokenSingleResponse
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck // md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
err := md.DB.QueryRow("SELECT id, privileges, description FROM tokens WHERE id = ?", md.User.ID).Scan( err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
&r.ID, &r.Privileges, &r.Description, &r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
) )
if err != nil { if err != nil {
md.Err(err) md.Err(err)
@@ -189,18 +142,37 @@ func TokenSelfGET(md common.MethodData) common.CodeMessager {
return r return r
} }
// TokenFixPrivilegesGET fixes the privileges on the token of the given user, func getBearerToken(md common.MethodData) common.CodeMessager {
var b bearerTokenSingleResponse
err := md.DB.
QueryRow(`
SELECT t.scope, t.created_at, c.id, c.extra
FROM osin_access t INNER JOIN osin_client c
WHERE t.access_token = ?
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
)
if err != nil {
md.Err(err)
return Err500
}
b.Code = 200
b.Privileges = md.User.TokenPrivileges
return b
}
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
// or of all the users if no user is given. // or of all the users if no user is given.
func TokenFixPrivilegesGET(md common.MethodData) common.CodeMessager { func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
id := common.Int(md.C.Query("id")) id := common.Int(md.Query("id"))
if md.C.Query("id") == "self" { if md.Query("id") == "self" {
id = md.ID() id = md.ID()
} }
go fixPrivileges(id, md.DB) go fixPrivileges(id, md.DB)
return common.SimpleResponse(200, "Privilege fixing started!") return common.SimpleResponse(200, "Privilege fixing started!")
} }
func fixPrivileges(user int, db *sql.DB) { func fixPrivileges(user int, db *sqlx.DB) {
var wc string var wc string
var params = make([]interface{}, 0, 1) var params = make([]interface{}, 0, 1)
if user != 0 { if user != 0 {
@@ -210,7 +182,7 @@ func fixPrivileges(user int, db *sql.DB) {
} }
rows, err := db.Query(` rows, err := db.Query(`
SELECT SELECT
tokens.id, tokens.privileges, users.rank tokens.id, tokens.privileges, users.privileges
FROM tokens FROM tokens
LEFT JOIN users ON users.id = tokens.user LEFT JOIN users ON users.id = tokens.user
`+wc, params...) `+wc, params...)
@@ -225,11 +197,16 @@ LEFT JOIN users ON users.id = tokens.user
privsRaw uint64 privsRaw uint64
privs common.Privileges privs common.Privileges
newPrivs common.Privileges newPrivs common.Privileges
rank int privilegesRaw uint64
) )
rows.Scan(&id, &privsRaw, &rank) err := rows.Scan(&id, &privsRaw, &privilegesRaw)
if err != nil {
fmt.Println(err)
continue
}
privileges := common.UserPrivileges(privilegesRaw)
privs = common.Privileges(privsRaw) privs = common.Privileges(privsRaw)
newPrivs = privs.CanOnly(rank) newPrivs = privs.CanOnly(privileges)
if newPrivs != privs { if newPrivs != privs {
_, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id) _, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id)
if err != nil { if err != nil {

View File

@@ -5,56 +5,92 @@ import (
"database/sql" "database/sql"
"strconv" "strconv"
"strings" "strings"
"time" "unicode"
"git.zxq.co/ripple/ocl" "github.com/jmoiron/sqlx"
"git.zxq.co/ripple/rippleapi/common" "zxq.co/ripple/ocl"
"github.com/osuyozora/api/common"
) )
type userData struct { type userData struct {
ID int `json:"id"` ID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
UsernameAKA string `json:"username_aka"` UsernameAKA string `json:"username_aka"`
RegisteredOn time.Time `json:"registered_on"` RegisteredOn common.UnixTimestamp `json:"registered_on"`
Rank int `json:"rank"` Privileges uint64 `json:"privileges"`
LatestActivity time.Time `json:"latest_activity"` LatestActivity common.UnixTimestamp `json:"latest_activity"`
Country string `json:"country"` Country string `json:"country"`
} }
const userFields = `SELECT users.id, users.username, register_datetime, users.privileges,
latest_activity, users_stats.username_aka,
users_stats.country
FROM users
INNER JOIN users_stats
ON users.id=users_stats.id
`
type userNotFullResponseLmao struct {
Id int `json:"id"`
Username string `json:"username"`
UsernameAKA string `json:"username_aka"`
RegisteredOn common.UnixTimestamp `json:"registered_on"`
Privileges uint64 `json:"privileges"`
LatestActivity common.UnixTimestamp `json:"latest_activity"`
Country string `json:"country"`
UserColor string `json:"user_color"`
RankedScoreStd uint64 `json:"ranked_score_std"`
TotalScoreStd uint64 `json:"total_score_std"`
PlaycountStd int `json:"playcount_std"`
ReplaysWatchedStd int `json:"replays_watched_std"`
TotalHitsStd int `json:"total_hits_std"`
PpStd int `json:"pp_std"`
RankedScoreTaiko uint64 `json:"ranked_score_taiko"`
TotalScoreTaiko uint64 `json:"total_score_taiko"`
PlaycountTaiko int `json:"playcount_taiko"`
ReplaysWatchedTaiko int `json:"replays_watched_taiko"`
TotalHitsTaiko int `json:"total_hits_taiko"`
PpTaiko int `json:"pp_taiko"`
RankedScoreCtb uint64 `json:"ranked_score_ctb"`
TotalScoreCtb uint64 `json:"total_score_ctb"`
PlaycountCtb int `json:"playcount_ctb"`
ReplaysWatchedCtb int `json:"replays_watched_ctb"`
TotalHitsCtb int `json:"total_hits_ctb"`
PpCtb int `json:"pp_ctb"`
RankedScoreMania uint64 `json:"ranked_score_mania"`
TotalScoreMania uint64 `json:"total_score_mania"`
PlaycountMania int `json:"playcount_mania"`
ReplaysWatchedMania int `json:"replays_watched_mania"`
TotalHitsMania int `json:"total_hits_mania"`
PpMania int `json:"pp_mania"`
// STD clappedModeData `json:"std"`
// Taiko clappedModeData `json:"taiko"`
// CTB clappedModeData `json:"ctb"`
// Mania clappedModeData `json:"mania"`
}
// UsersGET is the API handler for GET /users // UsersGET is the API handler for GET /users
func UsersGET(md common.MethodData) common.CodeMessager { func UsersGET(md common.MethodData) common.CodeMessager {
shouldRet, whereClause, param := whereClauseUser(md, "users") shouldRet, whereClause, param := whereClauseUser(md, "users")
if shouldRet != nil { if shouldRet != nil {
return *shouldRet return userPutsMulti(md)
} }
query := ` query := userFields + `
SELECT users.id, users.username, register_datetime, rank, WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
latest_activity, users_stats.username_aka,
users_stats.country, users_stats.show_country
FROM users
LEFT JOIN users_stats
ON users.id=users_stats.id
WHERE ` + whereClause + ` AND users.allowed='1'
LIMIT 1` LIMIT 1`
return userPuts(md, md.DB.QueryRow(query, param)) return userPutsSingle(md, md.DB.QueryRowx(query, param))
} }
type userPutsUserData struct { type userPutsSingleUserData struct {
common.ResponseBase common.ResponseBase
userData userData
} }
func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager { func userPutsSingle(md common.MethodData, row *sqlx.Row) common.CodeMessager {
var err error var err error
var user userPutsUserData var user userPutsSingleUserData
var ( err = row.StructScan(&user.userData)
registeredOn int64
latestActivity int64
showCountry bool
)
err = row.Scan(&user.ID, &user.Username, &registeredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showCountry)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return common.SimpleResponse(404, "No such user was found!") return common.SimpleResponse(404, "No such user was found!")
@@ -63,44 +99,91 @@ func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager {
return Err500 return Err500
} }
user.RegisteredOn = time.Unix(registeredOn, 0)
user.LatestActivity = time.Unix(latestActivity, 0)
user.Country = genCountry(md, user.ID, showCountry, user.Country)
user.Code = 200 user.Code = 200
return user return user
} }
func badgesToArray(badges string) []int { type userPutsMultiUserData struct {
var end []int common.ResponseBase
badgesSl := strings.Split(badges, ",") Users []userData `json:"users"`
for _, badge := range badgesSl {
if badge != "" && badge != "0" {
nb := common.Int(badge)
if nb != 0 {
end = append(end, nb)
}
}
}
return end
} }
func genCountry(md common.MethodData, uid int, showCountry bool, country string) string { func userPutsMulti(md common.MethodData) common.CodeMessager {
// If the user wants to stay anonymous, don't show their country. pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
// This can be overriden if we have the ReadConfidential privilege and the user we are accessing is the token owner. // query composition
if showCountry || (md.User.Privileges.HasPrivilegeReadConfidential() && uid == md.ID()) { wh := common.
return country Where("users.username_safe = ?", common.SafeUsername(md.Query("nname"))).
Where("users.id = ?", md.Query("iid")).
Where("users.privileges = ?", md.Query("privileges")).
Where("users.privileges & ? > 0", md.Query("has_privileges")).
Where("users.privileges & ? = 0", md.Query("has_not_privileges")).
Where("users_stats.country = ?", md.Query("country")).
Where("users_stats.username_aka = ?", md.Query("name_aka")).
Where("privileges_groups.name = ?", md.Query("privilege_group")).
In("users.id", pm("ids")...).
In("users.username_safe", safeUsernameBulk(pm("names"))...).
In("users_stats.username_aka", pm("names_aka")...).
In("users_stats.country", pm("countries")...)
var extraJoin string
if md.Query("privilege_group") != "" {
extraJoin = " LEFT JOIN privileges_groups ON users.privileges & privileges_groups.privileges = privileges_groups.privileges "
} }
return "XX"
query := userFields + extraJoin + wh.ClauseSafe() + " AND " + md.User.OnlyUserPublic(true) +
" " + common.Sort(md, common.SortConfiguration{
Allowed: []string{
"id",
"username",
"privileges",
"donor_expire",
"latest_activity",
"silence_end",
},
Default: "id ASC",
Table: "users",
}) +
" " + common.Paginate(md.Query("p"), md.Query("l"), 100)
// query execution
rows, err := md.DB.Queryx(query, wh.Params...)
if err != nil {
md.Err(err)
return Err500
}
var r userPutsMultiUserData
for rows.Next() {
var u userData
err := rows.StructScan(&u)
if err != nil {
md.Err(err)
continue
}
r.Users = append(r.Users, u)
}
r.Code = 200
return r
} }
// UserSelfGET is a shortcut for /users/id/self. (/users/self) // UserSelfGET is a shortcut for /users/id/self. (/users/self)
func UserSelfGET(md common.MethodData) common.CodeMessager { func UserSelfGET(md common.MethodData) common.CodeMessager {
md.C.Request.URL.RawQuery = "id=self&" + md.C.Request.URL.RawQuery md.Ctx.Request.URI().SetQueryString("id=self")
return UsersGET(md) return UsersGET(md)
} }
func safeUsernameBulk(us [][]byte) [][]byte {
for _, u := range us {
for idx, v := range u {
if v == ' ' {
u[idx] = '_'
continue
}
u[idx] = byte(unicode.ToLower(rune(v)))
}
}
return us
}
type whatIDResponse struct { type whatIDResponse struct {
common.ResponseBase common.ResponseBase
ID int `json:"id"` ID int `json:"id"`
@@ -110,16 +193,24 @@ type whatIDResponse struct {
func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager { func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
var ( var (
r whatIDResponse r whatIDResponse
allowed int privileges uint64
) )
err := md.DB.QueryRow("SELECT id, allowed FROM users WHERE username = ? LIMIT 1", md.C.Query("name")).Scan(&r.ID, &allowed) err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username_safe = ? LIMIT 1", common.SafeUsername(md.Query("name"))).Scan(&r.ID, &privileges)
if err != nil || (allowed != 1 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) { if err != nil || ((privileges&uint64(common.UserPrivilegePublic)) == 0 &&
(md.User.UserPrivileges&common.AdminPrivilegeManageUsers == 0)) {
return common.SimpleResponse(404, "That user could not be found!") return common.SimpleResponse(404, "That user could not be found!")
} }
r.Code = 200 r.Code = 200
return r return r
} }
var modesToReadable = [...]string{
"std",
"taiko",
"ctb",
"mania",
}
type modeData struct { type modeData struct {
RankedScore uint64 `json:"ranked_score"` RankedScore uint64 `json:"ranked_score"`
TotalScore uint64 `json:"total_score"` TotalScore uint64 `json:"total_score"`
@@ -129,8 +220,10 @@ type modeData struct {
Level float64 `json:"level"` Level float64 `json:"level"`
Accuracy float64 `json:"accuracy"` Accuracy float64 `json:"accuracy"`
PP int `json:"pp"` PP int `json:"pp"`
GlobalLeaderboardRank int `json:"global_leaderboard_rank"` GlobalLeaderboardRank *int `json:"global_leaderboard_rank"`
CountryLeaderboardRank *int `json:"country_leaderboard_rank"`
} }
type userFullResponse struct { type userFullResponse struct {
common.ResponseBase common.ResponseBase
userData userData
@@ -140,10 +233,21 @@ type userFullResponse struct {
Mania modeData `json:"mania"` Mania modeData `json:"mania"`
PlayStyle int `json:"play_style"` PlayStyle int `json:"play_style"`
FavouriteMode int `json:"favourite_mode"` FavouriteMode int `json:"favourite_mode"`
Badges []int `json:"badges"` Badges []singleBadge `json:"badges"`
Clan singleClan `json:"clan"`
CustomBadge *singleBadge `json:"custom_badge"`
SilenceInfo silenceInfo `json:"silence_info"`
CMNotes *string `json:"cm_notes,omitempty"`
BanDate *common.UnixTimestamp `json:"ban_date,omitempty"`
Email string `json:"email,omitempty"`
} }
// UserFullGET gets all of an user's information, with one exception: their userpage. type silenceInfo struct {
Reason string `json:"reason"`
End common.UnixTimestamp `json:"end"`
}
// Rx gets all of an user's information, with one exception: their userpage.
func UserFullGET(md common.MethodData) common.CodeMessager { func UserFullGET(md common.MethodData) common.CodeMessager {
shouldRet, whereClause, param := whereClauseUser(md, "users") shouldRet, whereClause, param := whereClauseUser(md, "users")
if shouldRet != nil { if shouldRet != nil {
@@ -153,71 +257,71 @@ func UserFullGET(md common.MethodData) common.CodeMessager {
// Hellest query I've ever done. // Hellest query I've ever done.
query := ` query := `
SELECT SELECT
users.id, users.username, users.register_datetime, users.rank, users.latest_activity, users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.badges_shown, users_stats.country, users_stats.show_country, users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
users_stats.play_style, users_stats.favourite_mode,
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
users_stats.show_custom_badge,
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.playcount_std, users_stats.ranked_score_std, users_stats.total_score_std, users_stats.playcount_std,
users_stats.replays_watched_std, users_stats.total_hits_std, users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.avg_accuracy_std, users_stats.pp_std, leaderboard_std.position as std_position, users_stats.avg_accuracy_std, users_stats.pp_std,
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.playcount_taiko, users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.playcount_taiko,
users_stats.replays_watched_taiko, users_stats.total_hits_taiko, users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
users_stats.avg_accuracy_taiko, users_stats.pp_taiko, leaderboard_taiko.position as taiko_position, users_stats.avg_accuracy_taiko, users_stats.pp_taiko,
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.playcount_ctb, users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.playcount_ctb,
users_stats.replays_watched_ctb, users_stats.total_hits_ctb, users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
users_stats.avg_accuracy_ctb, users_stats.pp_ctb, leaderboard_ctb.position as ctb_position, users_stats.avg_accuracy_ctb, users_stats.pp_ctb,
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania, users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
users_stats.replays_watched_mania, users_stats.total_hits_mania, users_stats.replays_watched_mania, users_stats.total_hits_mania,
users_stats.avg_accuracy_mania, users_stats.pp_mania, leaderboard_mania.position as mania_position users_stats.avg_accuracy_mania, users_stats.pp_mania,
users.silence_reason, users.silence_end,
users.notes, users.ban_datetime, users.email
FROM users FROM users
LEFT JOIN users_stats LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
LEFT JOIN leaderboard_std WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
ON users.id=leaderboard_std.user
LEFT JOIN leaderboard_taiko
ON users.id=leaderboard_taiko.user
LEFT JOIN leaderboard_ctb
ON users.id=leaderboard_ctb.user
LEFT JOIN leaderboard_mania
ON users.id=leaderboard_mania.user
WHERE ` + whereClause + ` AND users.allowed = '1'
LIMIT 1 LIMIT 1
` `
// Fuck. // Fuck.
r := userFullResponse{} r := userFullResponse{}
var ( var (
badges string b singleBadge
country string can bool
showCountry bool show bool
registeredOn int64
latestActivity int64
) )
err := md.DB.QueryRow(query, param).Scan( err := md.DB.QueryRow(query, param).Scan(
&r.ID, &r.Username, &registeredOn, &r.Rank, &latestActivity, &r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
&r.UsernameAKA, &badges, &country, &showCountry, &r.UsernameAKA, &r.Country,
&r.PlayStyle, &r.FavouriteMode, &r.PlayStyle, &r.FavouriteMode,
&b.Icon, &b.Name, &can, &show,
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount, &r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
&r.STD.ReplaysWatched, &r.STD.TotalHits, &r.STD.ReplaysWatched, &r.STD.TotalHits,
&r.STD.Accuracy, &r.STD.PP, &r.STD.GlobalLeaderboardRank, &r.STD.Accuracy, &r.STD.PP,
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount, &r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits, &r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
&r.Taiko.Accuracy, &r.Taiko.PP, &r.Taiko.GlobalLeaderboardRank, &r.Taiko.Accuracy, &r.Taiko.PP,
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount, &r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
&r.CTB.ReplaysWatched, &r.CTB.TotalHits, &r.CTB.ReplaysWatched, &r.CTB.TotalHits,
&r.CTB.Accuracy, &r.CTB.PP, &r.CTB.GlobalLeaderboardRank, &r.CTB.Accuracy, &r.CTB.PP,
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount, &r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
&r.Mania.ReplaysWatched, &r.Mania.TotalHits, &r.Mania.ReplaysWatched, &r.Mania.TotalHits,
&r.Mania.Accuracy, &r.Mania.PP, &r.Mania.GlobalLeaderboardRank, &r.Mania.Accuracy, &r.Mania.PP,
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
&r.CMNotes, &r.BanDate, &r.Email,
) )
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
@@ -227,14 +331,336 @@ LIMIT 1
return Err500 return Err500
} }
r.Country = genCountry(md, r.ID, showCountry, country) can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
r.Badges = badgesToArray(badges) if can && (b.Name != "" || b.Icon != "") {
r.CustomBadge = &b
}
r.RegisteredOn = time.Unix(registeredOn, 0) for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
r.LatestActivity = time.Unix(latestActivity, 0)
for _, m := range []*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore)) m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
if i := leaderboardPosition(md.R, modesToReadable[modeID], r.ID); i != nil {
m.GlobalLeaderboardRank = i
}
if i := countryPosition(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
m.CountryLeaderboardRank = i
}
}
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var badge singleBadge
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
if err != nil {
md.Err(err)
continue
}
r.Badges = append(r.Badges, badge)
}
if md.User.TokenPrivileges&common.PrivilegeManageUser == 0 {
r.CMNotes = nil
r.BanDate = nil
r.Email = ""
}
rows, err = md.DB.Query("SELECT c.id, c.name, c.description, c.tag, c.icon FROM user_clans uc "+
"LEFT JOIN clans c ON uc.clan = c.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var clan singleClan
err = rows.Scan(&clan.ID, &clan.Name, &clan.Description, &clan.Tag, &clan.Icon)
if err != nil {
md.Err(err)
continue
}
r.Clan = clan
}
r.Code = 200
return r
}
func UserFullGETRx(md common.MethodData) common.CodeMessager {
shouldRet, whereClause, param := whereClauseUser(md, "users")
if shouldRet != nil {
return *shouldRet
}
// Hellest query I've ever done.
query := `
SELECT
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
users_stats.show_custom_badge,
users_stats.ranked_score_std_rx, users_stats.total_score_std_rx, users_stats.playcount_std_rx,
users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.avg_accuracy_std_rx, users_stats.pp_std_rx,
users_stats.ranked_score_taiko_rx, users_stats.total_score_taiko_rx, users_stats.playcount_taiko_rx,
users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
users_stats.avg_accuracy_taiko_rx, users_stats.pp_taiko_rx,
users_stats.ranked_score_ctb_rx, users_stats.total_score_ctb_rx, users_stats.playcount_ctb_rx,
users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
users_stats.avg_accuracy_ctb_rx, users_stats.pp_ctb_rx,
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
users_stats.replays_watched_mania, users_stats.total_hits_mania,
users_stats.avg_accuracy_mania, users_stats.pp_mania,
users.silence_reason, users.silence_end,
users.notes, users.ban_datetime, users.email
FROM users
LEFT JOIN users_stats
ON users.id=users_stats.id
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
LIMIT 1
`
// Fuck.
r := userFullResponse{}
var (
b singleBadge
can bool
show bool
)
err := md.DB.QueryRow(query, param).Scan(
&r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
&r.UsernameAKA, &r.Country,
&r.PlayStyle, &r.FavouriteMode,
&b.Icon, &b.Name, &can, &show,
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
&r.STD.ReplaysWatched, &r.STD.TotalHits,
&r.STD.Accuracy, &r.STD.PP,
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
&r.Taiko.Accuracy, &r.Taiko.PP,
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
&r.CTB.ReplaysWatched, &r.CTB.TotalHits,
&r.CTB.Accuracy, &r.CTB.PP,
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
&r.Mania.ReplaysWatched, &r.Mania.TotalHits,
&r.Mania.Accuracy, &r.Mania.PP,
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
&r.CMNotes, &r.BanDate, &r.Email,
)
switch {
case err == sql.ErrNoRows:
return common.SimpleResponse(404, "That user could not be found!")
case err != nil:
md.Err(err)
return Err500
}
can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
if can && (b.Name != "" || b.Icon != "") {
r.CustomBadge = &b
}
for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
if i := leaderboardPositionRx(md.R, modesToReadable[modeID], r.ID); i != nil {
m.GlobalLeaderboardRank = i
}
if i := countryPositionRx(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
m.CountryLeaderboardRank = i
}
}
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var badge singleBadge
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
if err != nil {
md.Err(err)
continue
}
r.Badges = append(r.Badges, badge)
}
if md.User.TokenPrivileges&common.PrivilegeManageUser == 0 {
r.CMNotes = nil
r.BanDate = nil
r.Email = ""
}
rows, err = md.DB.Query("SELECT c.id, c.name, c.description, c.tag, c.icon FROM user_clans uc "+
"LEFT JOIN clans c ON uc.clan = c.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var clan singleClan
err = rows.Scan(&clan.ID, &clan.Name, &clan.Description, &clan.Tag, &clan.Icon)
if err != nil {
md.Err(err)
continue
}
r.Clan = clan
}
r.Code = 200
return r
}
func UserFullGETAp(md common.MethodData) common.CodeMessager {
shouldRet, whereClause, param := whereClauseUser(md, "users")
if shouldRet != nil {
return *shouldRet
}
// Hellest query I've ever done.
query := `
SELECT
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
users_stats.show_custom_badge,
users_stats.ranked_score_std_ap, users_stats.total_score_std, users_stats.playcount_std_ap,
users_stats.replays_watched_std, users_stats.total_hits_std,
users_stats.avg_accuracy_std_ap, users_stats.pp_std_auto,
users_stats.ranked_score_taiko_ap, users_stats.total_score_taiko, users_stats.playcount_taiko_ap,
users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
users_stats.avg_accuracy_taiko_ap, users_stats.pp_taiko_auto,
users_stats.ranked_score_ctb_ap, users_stats.total_score_ctb, users_stats.playcount_ctb_ap,
users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
users_stats.avg_accuracy_ctb_ap, users_stats.pp_ctb_auto,
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
users_stats.replays_watched_mania, users_stats.total_hits_mania,
users_stats.avg_accuracy_mania, users_stats.pp_mania,
users.silence_reason, users.silence_end,
users.notes, users.ban_datetime, users.email
FROM users
LEFT JOIN users_stats
ON users.id=users_stats.id
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
LIMIT 1
`
// Fuck.
r := userFullResponse{}
var (
b singleBadge
can bool
show bool
)
err := md.DB.QueryRow(query, param).Scan(
&r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
&r.UsernameAKA, &r.Country,
&r.PlayStyle, &r.FavouriteMode,
&b.Icon, &b.Name, &can, &show,
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
&r.STD.ReplaysWatched, &r.STD.TotalHits,
&r.STD.Accuracy, &r.STD.PP,
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
&r.Taiko.Accuracy, &r.Taiko.PP,
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
&r.CTB.ReplaysWatched, &r.CTB.TotalHits,
&r.CTB.Accuracy, &r.CTB.PP,
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
&r.Mania.ReplaysWatched, &r.Mania.TotalHits,
&r.Mania.Accuracy, &r.Mania.PP,
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
&r.CMNotes, &r.BanDate, &r.Email,
)
switch {
case err == sql.ErrNoRows:
return common.SimpleResponse(404, "That user could not be found!")
case err != nil:
md.Err(err)
return Err500
}
can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
if can && (b.Name != "" || b.Icon != "") {
r.CustomBadge = &b
}
for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
if i := leaderboardPositionAp(md.R, modesToReadable[modeID], r.ID); i != nil {
m.GlobalLeaderboardRank = i
}
if i := countryPositionAp(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
m.CountryLeaderboardRank = i
}
}
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var badge singleBadge
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
if err != nil {
md.Err(err)
continue
}
r.Badges = append(r.Badges, badge)
}
if md.User.TokenPrivileges&common.PrivilegeManageUser == 0 {
r.CMNotes = nil
r.BanDate = nil
r.Email = ""
}
rows, err = md.DB.Query("SELECT c.id, c.name, c.description, c.tag, c.icon FROM user_clans uc "+
"LEFT JOIN clans c ON uc.clan = c.id WHERE user = ?", r.ID)
if err != nil {
md.Err(err)
}
for rows.Next() {
var clan singleClan
err = rows.Scan(&clan.ID, &clan.Name, &clan.Description, &clan.Tag, &clan.Icon)
if err != nil {
md.Err(err)
continue
}
r.Clan = clan
} }
r.Code = 200 r.Code = 200
@@ -243,7 +669,7 @@ LIMIT 1
type userpageResponse struct { type userpageResponse struct {
common.ResponseBase common.ResponseBase
Userpage string `json:"userpage"` Userpage *string `json:"userpage"`
} }
// UserUserpageGET gets an user's userpage, as in the customisable thing. // UserUserpageGET gets an user's userpage, as in the customisable thing.
@@ -261,23 +687,44 @@ func UserUserpageGET(md common.MethodData) common.CodeMessager {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
if r.Userpage == nil {
r.Userpage = new(string)
}
r.Code = 200 r.Code = 200
return r return r
} }
// UserSelfUserpagePOST allows to change the current user's userpage.
func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
var d struct {
Data *string `json:"data"`
}
md.Unmarshal(&d)
if d.Data == nil {
return ErrMissingField("data")
}
cont := common.SanitiseString(*d.Data)
_, err := md.DB.Exec("UPDATE users_stats SET userpage_content = ? WHERE id = ? LIMIT 1", cont, md.ID())
if err != nil {
md.Err(err)
}
md.Ctx.URI().SetQueryString("id=self")
return UserUserpageGET(md)
}
func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) { func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) {
switch { switch {
case md.C.Query("id") == "self": case md.Query("id") == "self":
return nil, tableName + ".id = ?", md.ID() return nil, tableName + ".id = ?", md.ID()
case md.C.Query("id") != "": case md.Query("id") != "":
id, err := strconv.Atoi(md.C.Query("id")) id, err := strconv.Atoi(md.Query("id"))
if err != nil { if err != nil {
a := common.SimpleResponse(400, "please pass a valid user ID") a := common.SimpleResponse(400, "please pass a valid user ID")
return &a, "", nil return &a, "", nil
} }
return nil, tableName + ".id = ?", id return nil, tableName + ".id = ?", id
case md.C.Query("name") != "": case md.Query("name") != "":
return nil, tableName + ".username = ?", md.C.Query("name") return nil, tableName + ".username_safe = ?", common.SafeUsername(md.Query("name"))
} }
a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id") a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id")
return &a, "", nil return &a, "", nil
@@ -295,23 +742,32 @@ type lookupUser struct {
// UserLookupGET does a quick lookup of users beginning with the passed // UserLookupGET does a quick lookup of users beginning with the passed
// querystring value name. // querystring value name.
func UserLookupGET(md common.MethodData) common.CodeMessager { func UserLookupGET(md common.MethodData) common.CodeMessager {
name := strings.NewReplacer( name := common.SafeUsername(md.Query("name"))
name = strings.NewReplacer(
"%", "\\%", "%", "\\%",
"_", "\\_", "_", "\\_",
"\\", "\\\\", "\\", "\\\\",
).Replace(md.C.Query("name")) ).Replace(name)
if name == "" { if name == "" {
return common.SimpleResponse(400, "please provide an username to start searching") return common.SimpleResponse(400, "please provide an username to start searching")
} }
name = "%" + name + "%" name = "%" + name + "%"
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE username LIKE ? AND allowed = '1' LIMIT 25", name)
var email string
if md.User.TokenPrivileges&common.PrivilegeManageUser != 0 &&
strings.Contains(md.Query("name"), "@") {
email = md.Query("name")
}
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE "+
"(username_safe LIKE ? OR email = ?) AND "+
md.User.OnlyUserPublic(true)+" LIMIT 25", name, email)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
var r userLookupResponse var r userLookupResponse
for rows.Next() { for rows.Next() {
var l lookupUser var l lookupUser
err := rows.Scan(&l.ID, &l.Username) err := rows.Scan(&l.ID, &l.Username)

View File

@@ -0,0 +1,84 @@
package v1
import (
"database/sql"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/osuyozora/api/common"
)
// Achievement represents an achievement in the database.
type Achievement struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
}
// LoadAchievementsEvery reloads the achievements in the database every given
// amount of time.
func LoadAchievementsEvery(db *sqlx.DB, d time.Duration) {
for {
achievs = nil
err := db.Select(&achievs,
"SELECT id, name, description, icon FROM achievements ORDER BY id ASC")
if err != nil {
fmt.Println("LoadAchievements error", err)
common.GenericError(err)
}
time.Sleep(d)
}
}
var achievs []Achievement
type userAchievement struct {
Achievement
Achieved bool `json:"achieved"`
}
type userAchievementsResponse struct {
common.ResponseBase
Achievements []userAchievement `json:"achievements"`
}
// UserAchievementsGET handles requests for retrieving the achievements of a
// given user.
func UserAchievementsGET(md common.MethodData) common.CodeMessager {
shouldRet, whereClause, param := whereClauseUser(md, "users")
if shouldRet != nil {
return *shouldRet
}
var ids []int
err := md.DB.Select(&ids, `SELECT ua.achievement_id FROM users_achievements ua
INNER JOIN users ON users.id = ua.user_id
WHERE `+whereClause+` ORDER BY ua.achievement_id ASC`, param)
switch {
case err == sql.ErrNoRows:
return common.SimpleResponse(404, "No such user!")
case err != nil:
md.Err(err)
return Err500
}
all := md.HasQuery("all")
resp := userAchievementsResponse{Achievements: make([]userAchievement, 0, len(achievs))}
for _, ach := range achievs {
achieved := inInt(ach.ID, ids)
if all || achieved {
resp.Achievements = append(resp.Achievements, userAchievement{ach, achieved})
}
}
resp.Code = 200
return resp
}
func inInt(i int, js []int) bool {
for _, j := range js {
if i == j {
return true
}
}
return false
}

View File

@@ -2,35 +2,16 @@ package v1
import ( import (
"fmt" "fmt"
"strconv" "strings"
"time"
"git.zxq.co/ripple/rippleapi/common" "gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
) )
type score struct {
ID int `json:"id"`
BeatmapMD5 string `json:"beatmap_md5"`
Score int64 `json:"score"`
MaxCombo int `json:"max_combo"`
FullCombo bool `json:"full_combo"`
Mods int `json:"mods"`
Count300 int `json:"count_300"`
Count100 int `json:"count_100"`
Count50 int `json:"count_50"`
CountGeki int `json:"count_geki"`
CountKatu int `json:"count_katu"`
CountMiss int `json:"count_miss"`
Time time.Time `json:"time"`
PlayMode int `json:"play_mode"`
Accuracy float64 `json:"accuracy"`
PP float32 `json:"pp"`
Completed int `json:"completed"`
}
type userScore struct { type userScore struct {
score Score
Beatmap *beatmap `json:"beatmap"` Beatmap beatmap `json:"beatmap"`
} }
type userScoresResponse struct { type userScoresResponse struct {
@@ -48,12 +29,13 @@ SELECT
scores.completed, scores.completed,
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5, beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty, beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked, beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
beatmaps.ranked_status_freezed, beatmaps.latest_update beatmaps.ranked_status_freezed, beatmaps.latest_update
FROM scores FROM scores
LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5 INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
LEFT JOIN users ON users.id = scores.userid INNER JOIN users ON users.id = scores.userid
` `
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if // UserScoresBestGET retrieves the best scores of an user, sorted by PP if
@@ -64,8 +46,8 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
return *cm return *cm
} }
mc := genModeClause(md) mc := genModeClause(md)
// Do not print 0pp scores on std // For all modes that have PP, we leave out 0 PP scores.
if getMode(md.C.Query("mode")) == "std" { if getMode(md.Query("mode")) != "ctb" {
mc += " AND scores.pp > 0" mc += " AND scores.pp > 0"
} }
return scoresPuts(md, fmt.Sprintf( return scoresPuts(md, fmt.Sprintf(
@@ -73,9 +55,9 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
scores.completed = '3' scores.completed = '3'
AND %s AND %s
%s %s
AND users.allowed = '1' AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores.pp DESC, scores.score DESC %s`, ORDER BY scores.pp DESC, scores.score DESC %s`,
wc, mc, common.Paginate(md.C.Query("p"), md.C.Query("l"), 100), wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
), param) ), param)
} }
@@ -89,36 +71,12 @@ func UserScoresRecentGET(md common.MethodData) common.CodeMessager {
`WHERE `WHERE
%s %s
%s %s
AND users.allowed = '1' AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores.time DESC %s`, ORDER BY scores.id DESC %s`,
wc, genModeClause(md), common.Paginate(md.C.Query("p"), md.C.Query("l"), 100), wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
), param) ), param)
} }
func getMode(m string) string {
switch m {
case "1":
return "taiko"
case "2":
return "ctb"
case "3":
return "mania"
default:
return "std"
}
}
func genModeClause(md common.MethodData) string {
var modeClause string
if md.C.Query("mode") != "" {
m, err := strconv.Atoi(md.C.Query("mode"))
if err == nil && m >= 0 && m <= 3 {
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
}
}
return modeClause
}
func scoresPuts(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager { func scoresPuts(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
rows, err := md.DB.Query(userScoreSelectBase+whereClause, params...) rows, err := md.DB.Query(userScoreSelectBase+whereClause, params...)
if err != nil { if err != nil {
@@ -129,39 +87,37 @@ func scoresPuts(md common.MethodData, whereClause string, params ...interface{})
for rows.Next() { for rows.Next() {
var ( var (
us userScore us userScore
t string b beatmap
b beatmapMayOrMayNotExist
rawLatestUpdate *int64
) )
err = rows.Scan( err = rows.Scan(
&us.ID, &us.BeatmapMD5, &us.Score, &us.ID, &us.BeatmapMD5, &us.Score.Score,
&us.MaxCombo, &us.FullCombo, &us.Mods, &us.MaxCombo, &us.FullCombo, &us.Mods,
&us.Count300, &us.Count100, &us.Count50, &us.Count300, &us.Count100, &us.Count50,
&us.CountGeki, &us.CountKatu, &us.CountMiss, &us.CountGeki, &us.CountKatu, &us.CountMiss,
&t, &us.PlayMode, &us.Accuracy, &us.PP, &us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
&us.Completed, &us.Completed,
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
&b.SongName, &b.AR, &b.OD, &b.Difficulty, &b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
&b.MaxCombo, &b.HitLength, &b.Ranked, &b.MaxCombo, &b.HitLength, &b.Ranked,
&b.RankedStatusFrozen, &rawLatestUpdate, &b.RankedStatusFrozen, &b.LatestUpdate,
) )
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
// puck feppy b.Difficulty = b.Diff2.STD
us.Time, err = time.Parse(common.OsuTimeFormat, t) us.Beatmap = b
if err != nil { us.Rank = strings.ToUpper(getrank.GetRank(
md.Err(err) osuapi.Mode(us.PlayMode),
return Err500 osuapi.Mods(us.Mods),
} us.Accuracy,
if rawLatestUpdate != nil { us.Count300,
// fml i should have used an inner join us.Count100,
xd := time.Unix(*rawLatestUpdate, 0) us.Count50,
b.LatestUpdate = &xd us.CountMiss,
} ))
us.Beatmap = b.toBeatmap()
scores = append(scores, us) scores = append(scores, us)
} }
r := userScoresResponse{} r := userScoresResponse{}

127
app/v1/user_scores_ap.go Normal file
View File

@@ -0,0 +1,127 @@
package v1
import (
"fmt"
"strings"
"gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
)
type userScoreAuto struct {
Score
Beatmap beatmap `json:"beatmap"`
}
type userScoresResponseAuto struct {
common.ResponseBase
Scores []userScoreAuto `json:"scores"`
}
const userScoreSelectBaseAp = `
SELECT
scores_auto.id, scores_auto.beatmap_md5, scores_auto.score,
scores_auto.max_combo, scores_auto.full_combo, scores_auto.mods,
scores_auto.300_count, scores_auto.100_count, scores_auto.50_count,
scores_auto.gekis_count, scores_auto.katus_count, scores_auto.misses_count,
scores_auto.time, scores_auto.play_mode, scores_auto.accuracy, scores_auto.pp,
scores_auto.completed,
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
beatmaps.ranked_status_freezed, beatmaps.latest_update
FROM scores_auto
INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores_auto.beatmap_md5
INNER JOIN users ON users.id = scores_auto.userid
`
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if
// mode is standard and sorted by ranked score otherwise.
func UserScoresBestAPGET(md common.MethodData) common.CodeMessager {
cm, wc, param := whereClauseUser(md, "users")
if cm != nil {
return *cm
}
mc := genModeClauseAp(md)
// For all modes that have PP, we leave out 0 PP scores_auto.
if getMode(md.Query("mode")) != "ctb" {
mc += " AND scores_auto.pp > 0"
}
return scoresPutsAp(md, fmt.Sprintf(
`WHERE
scores_auto.completed = '3'
AND %s
%s
AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores_auto.pp DESC, scores_auto.score DESC %s`,
wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
), param)
}
// UserScoresRecentGET retrieves an user's latest scores_auto.
func UserScoresRecentAPGET(md common.MethodData) common.CodeMessager {
cm, wc, param := whereClauseUser(md, "users")
if cm != nil {
return *cm
}
return scoresPutsAp(md, fmt.Sprintf(
`WHERE
%s
%s
AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores_auto.id DESC %s`,
wc, genModeClauseAp(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
), param)
}
func scoresPutsAp(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
rows, err := md.DB.Query(userScoreSelectBaseAp+whereClause, params...)
if err != nil {
md.Err(err)
return Err500
}
var scores []userScoreAuto
for rows.Next() {
var (
us userScoreAuto
b beatmap
)
err = rows.Scan(
&us.ID, &us.BeatmapMD5, &us.Score.Score,
&us.MaxCombo, &us.FullCombo, &us.Mods,
&us.Count300, &us.Count100, &us.Count50,
&us.CountGeki, &us.CountKatu, &us.CountMiss,
&us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
&us.Completed,
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
&b.MaxCombo, &b.HitLength, &b.Ranked,
&b.RankedStatusFrozen, &b.LatestUpdate,
)
if err != nil {
md.Err(err)
return Err500
}
b.Difficulty = b.Diff2.STD
us.Beatmap = b
us.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(us.PlayMode),
osuapi.Mods(us.Mods),
us.Accuracy,
us.Count300,
us.Count100,
us.Count50,
us.CountMiss,
))
scores = append(scores, us)
}
r := userScoresResponseAuto{}
r.Code = 200
r.Scores = scores
return r
}

127
app/v1/user_scores_relax.go Normal file
View File

@@ -0,0 +1,127 @@
package v1
import (
"fmt"
"strings"
"gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
)
type userScoreRx struct {
Score
Beatmap beatmap `json:"beatmap"`
}
type userScoresResponseRx struct {
common.ResponseBase
Scores []userScoreRx `json:"scores"`
}
const userScoreSelectBaseRelax = `
SELECT
scores_relax.id, scores_relax.beatmap_md5, scores_relax.score,
scores_relax.max_combo, scores_relax.full_combo, scores_relax.mods,
scores_relax.300_count, scores_relax.100_count, scores_relax.50_count,
scores_relax.gekis_count, scores_relax.katus_count, scores_relax.misses_count,
scores_relax.time, scores_relax.play_mode, scores_relax.accuracy, scores_relax.pp,
scores_relax.completed,
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
beatmaps.ranked_status_freezed, beatmaps.latest_update
FROM scores_relax
INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores_relax.beatmap_md5
INNER JOIN users ON users.id = scores_relax.userid
`
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if
// mode is standard and sorted by ranked score otherwise.
func UserScoresBestRelaxGET(md common.MethodData) common.CodeMessager {
cm, wc, param := whereClauseUser(md, "users")
if cm != nil {
return *cm
}
mc := genModeClauseRx(md)
// For all modes that have PP, we leave out 0 PP scores_relax.
if getMode(md.Query("mode")) != "ctb" {
mc += " AND scores_relax.pp > 0"
}
return scoresPutsRx(md, fmt.Sprintf(
`WHERE
scores_relax.completed = '3'
AND %s
%s
AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores_relax.pp DESC, scores_relax.score DESC %s`,
wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
), param)
}
// UserScoresRecentGET retrieves an user's latest scores_relax.
func UserScoresRecentRelaxGET(md common.MethodData) common.CodeMessager {
cm, wc, param := whereClauseUser(md, "users")
if cm != nil {
return *cm
}
return scoresPutsRx(md, fmt.Sprintf(
`WHERE
%s
%s
AND `+md.User.OnlyUserPublic(true)+`
ORDER BY scores_relax.id DESC %s`,
wc, genModeClauseRx(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
), param)
}
func scoresPutsRx(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
rows, err := md.DB.Query(userScoreSelectBaseRelax+whereClause, params...)
if err != nil {
md.Err(err)
return Err500
}
var scores []userScoreRx
for rows.Next() {
var (
us userScoreRx
b beatmap
)
err = rows.Scan(
&us.ID, &us.BeatmapMD5, &us.Score.Score,
&us.MaxCombo, &us.FullCombo, &us.Mods,
&us.Count300, &us.Count100, &us.Count50,
&us.CountGeki, &us.CountKatu, &us.CountMiss,
&us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
&us.Completed,
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
&b.MaxCombo, &b.HitLength, &b.Ranked,
&b.RankedStatusFrozen, &b.LatestUpdate,
)
if err != nil {
md.Err(err)
return Err500
}
b.Difficulty = b.Diff2.STD
us.Beatmap = b
us.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(us.PlayMode),
osuapi.Mods(us.Mods),
us.Accuracy,
us.Count300,
us.Count100,
us.Count50,
us.CountMiss,
))
scores = append(scores, us)
}
r := userScoresResponseRx{}
r.Code = 200
r.Scores = scores
return r
}

18
app/websockets/entry.go Normal file
View File

@@ -0,0 +1,18 @@
package websockets
import (
"github.com/leavengood/websocket"
"github.com/valyala/fasthttp"
)
var upgrader = websocket.FastHTTPUpgrader{
Handler: handler,
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
return true
},
}
// WebsocketV1Entry upgrades a connection to a websocket.
func WebsocketV1Entry(ctx *fasthttp.RequestCtx) {
upgrader.UpgradeHandler(ctx)
}

101
app/websockets/identify.go Normal file
View File

@@ -0,0 +1,101 @@
package websockets
import (
"crypto/md5"
"crypto/sha256"
"encoding/json"
"fmt"
"database/sql"
"github.com/osuyozora/api/common"
)
type websocketUser struct {
ID int `json:"id"`
Username string `json:"username"`
UserPrivileges uint64 `json:"user_privileges"`
TokenPrivileges uint64 `json:"token_privileges"`
ApplicationID *string `json:"application_id"`
}
type identifyMessage struct {
Token string `json:"token"`
IsBearer bool `json:"is_bearer"`
}
// Identify sets the identity of the user.
func Identify(c *conn, message incomingMessage) {
var idMsg identifyMessage
err := json.Unmarshal(message.Data, &idMsg)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var wsu websocketUser
if idMsg.IsBearer {
err = getBearerToken(idMsg.Token, &wsu)
} else {
err = db.Get(&wsu, `
SELECT
t.user as id, t.privileges as token_privileges,
u.username, u.privileges as user_privileges
FROM tokens t
INNER JOIN users u ON t.user = u.id
WHERE t.token = ?`, fmt.Sprintf("%x", md5.Sum([]byte(idMsg.Token))))
}
switch err {
case nil:
break
case sql.ErrNoRows:
c.WriteJSON(TypeNotFound, nil)
return
default:
common.WSErr(err)
c.WriteJSON(TypeUnexpectedError, nil)
return
}
wsu.TokenPrivileges = uint64(
common.Privileges(wsu.TokenPrivileges).CanOnly(
common.UserPrivileges(wsu.UserPrivileges),
),
)
c.Mtx.Lock()
c.User = &wsu
c.Mtx.Unlock()
c.WriteJSON(TypeIdentified, wsu)
}
func getBearerToken(token string, wsu *websocketUser) error {
var x struct {
Client string
Scope string
Extra int
}
err := db.Get(&x, "SELECT client, scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
if err != nil {
return err
}
var userInfo struct {
Username string
Privileges uint64
}
err = db.Get(&userInfo, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
if err != nil {
return err
}
wsu.ApplicationID = &x.Client
wsu.ID = x.Extra
wsu.Username = userInfo.Username
wsu.UserPrivileges = userInfo.Privileges
wsu.TokenPrivileges = uint64(common.OAuthPrivileges(x.Scope))
return nil
}

View File

@@ -0,0 +1,125 @@
package websockets
import (
"encoding/json"
"sync"
"sync/atomic"
"time"
"github.com/leavengood/websocket"
)
var stepNumber uint64
func handler(rawConn *websocket.Conn) {
defer catchPanic()
defer rawConn.Close()
step := atomic.AddUint64(&stepNumber, 1)
// 5 is a security margin in case
if step == (1<<10 - 5) {
atomic.StoreUint64(&stepNumber, 0)
}
c := &conn{
rawConn,
sync.Mutex{},
step | uint64(time.Now().UnixNano()<<10),
false,
nil,
}
c.WriteJSON(TypeConnected, nil)
defer cleanup(c.ID)
for {
var i incomingMessage
err := c.Conn.ReadJSON(&i)
if _, ok := err.(*websocket.CloseError); ok {
return
}
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
continue
}
f, ok := messageHandler[i.Type]
if !ok {
c.WriteJSON(TypeInvalidMessage, "invalid message type")
continue
}
if f != nil {
f(c, i)
}
}
}
type conn struct {
Conn *websocket.Conn
Mtx sync.Mutex
ID uint64
RestrictedVisible bool
User *websocketUser
}
func (c *conn) WriteJSON(t string, data interface{}) error {
c.Mtx.Lock()
err := c.Conn.WriteJSON(newMessage(t, data))
c.Mtx.Unlock()
return err
}
var messageHandler = map[string]func(c *conn, message incomingMessage){
TypeSubscribeScores: SubscribeScores,
TypeSubscribeMultiMatches: SubscribeMultiMatches,
TypeSetRestrictedVisibility: SetRestrictedVisibility,
TypeIdentify: Identify,
TypePing: pingHandler,
}
// Server Message Types
const (
TypeConnected = "connected"
TypeInvalidMessage = "invalid_message_type"
TypeUnexpectedError = "unexpected_error"
TypeNotFound = "not_found"
TypeSubscribedToScores = "subscribed_to_scores"
TypeNewScore = "new_score"
TypeSubscribedToMultiMatches = "subscribed_mp_complete_match"
TypeNewMatch = "new_completed_match"
TypeIdentified = "identified"
TypeRestrictedVisibilitySet = "restricted_visibility_set"
TypePong = "pong"
)
// Client Message Types
const (
TypeSubscribeScores = "subscribe_scores"
TypeSubscribeMultiMatches = "subscribe_mp_complete_match"
TypeIdentify = "identify"
TypeSetRestrictedVisibility = "set_restricted_visibility"
TypePing = "ping"
)
func pingHandler(c *conn, message incomingMessage) {
c.WriteJSON(TypePong, nil)
}
// Message is the wrapped information for a message sent to the client.
type Message struct {
Type string `json:"type"`
Data interface{} `json:"data,omitempty"`
}
func newMessage(t string, data interface{}) Message {
return Message{
Type: t,
Data: data,
}
}
type incomingMessage struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}

57
app/websockets/multi.go Normal file
View File

@@ -0,0 +1,57 @@
package websockets
import (
"encoding/json"
"fmt"
"sync"
)
// SubscribeMultiMatches subscribes to receiving information from completed
// games in multiplayer matches.
func SubscribeMultiMatches(c *conn, message incomingMessage) {
multiSubscriptionsMtx.Lock()
var found bool
for _, el := range multiSubscriptions {
if el.ID == c.ID {
found = true
break
}
}
// if it was not found, we need to add it
if !found {
multiSubscriptions = append(multiSubscriptions, c)
}
multiSubscriptionsMtx.Unlock()
c.WriteJSON(TypeSubscribedToMultiMatches, nil)
}
var multiSubscriptions []*conn
var multiSubscriptionsMtx = new(sync.RWMutex)
func matchRetriever() {
ps, err := red.Subscribe("api:mp_complete_match")
if err != nil {
fmt.Println(err)
}
for {
msg, err := ps.ReceiveMessage()
if err != nil {
fmt.Println(err.Error())
return
}
go handleNewMultiGame(msg.Payload)
}
}
func handleNewMultiGame(payload string) {
defer catchPanic()
multiSubscriptionsMtx.RLock()
cp := make([]*conn, len(multiSubscriptions))
copy(cp, multiSubscriptions)
multiSubscriptionsMtx.RUnlock()
for _, el := range cp {
el.WriteJSON(TypeNewMatch, json.RawMessage(payload))
}
}

View File

@@ -0,0 +1,31 @@
package websockets
import (
"encoding/json"
"github.com/osuyozora/api/common"
)
// SetRestrictedVisibility sets whether the information of restricted users
// can be seen.
func SetRestrictedVisibility(c *conn, message incomingMessage) {
var visibility bool
err := json.Unmarshal(message.Data, &visibility)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var userIsManager bool
if c.User != nil && (c.User.UserPrivileges&uint64(common.AdminPrivilegeManageUsers) > 0) {
userIsManager = true
}
c.Mtx.Lock()
visibility = visibility && userIsManager
c.RestrictedVisible = visibility
c.Mtx.Unlock()
c.WriteJSON(TypeRestrictedVisibilitySet, visibility)
}

173
app/websockets/scores.go Normal file
View File

@@ -0,0 +1,173 @@
package websockets
import (
"encoding/json"
"fmt"
"strings"
"sync"
"gopkg.in/thehowl/go-osuapi.v1"
"github.com/osuyozora/api/app/v1"
"github.com/osuyozora/api/common"
"zxq.co/x/getrank"
)
type subscribeScoresUser struct {
User int `json:"user"`
Modes []int `json:"modes"`
}
// SubscribeScores subscribes a connection to score updates.
func SubscribeScores(c *conn, message incomingMessage) {
var ssu []subscribeScoresUser
err := json.Unmarshal(message.Data, &ssu)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
scoreSubscriptionsMtx.Lock()
var found bool
for idx, el := range scoreSubscriptions {
// already exists, change the users
if el.Conn.ID == c.ID {
found = true
scoreSubscriptions[idx].Users = ssu
}
}
// if it was not found, we need to add it
if !found {
scoreSubscriptions = append(scoreSubscriptions, scoreSubscription{c, ssu})
}
scoreSubscriptionsMtx.Unlock()
c.WriteJSON(TypeSubscribedToScores, ssu)
}
type scoreSubscription struct {
Conn *conn
Users []subscribeScoresUser
}
var scoreSubscriptions []scoreSubscription
var scoreSubscriptionsMtx = new(sync.RWMutex)
func scoreRetriever() {
ps, err := red.Subscribe("api:score_submission")
if err != nil {
fmt.Println(err)
}
for {
msg, err := ps.ReceiveMessage()
if err != nil {
fmt.Println(err.Error())
return
}
go handleNewScore(msg.Payload)
}
}
type scoreUser struct {
UserID int `json:"id"`
Username string `json:"username"`
Privileges uint64 `json:"privileges"`
}
type score struct {
v1.Score
scoreUser
}
type scoreJSON struct {
v1.Score
UserID int `json:"user_id"`
User scoreUser `json:"user"`
}
func handleNewScore(id string) {
defer catchPanic()
var s score
err := db.Get(&s, `
SELECT
s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods,
s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count,
s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id,
u.username, u.privileges
FROM scores s
INNER JOIN users u ON s.userid = u.id
WHERE s.id = ?`, id)
if err != nil {
fmt.Println(err)
return
}
s.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(s.PlayMode),
osuapi.Mods(s.Mods),
s.Accuracy,
s.Count300,
s.Count100,
s.Count50,
s.CountMiss,
))
sj := scoreJSON{
Score: s.Score,
UserID: s.UserID,
User: s.scoreUser,
}
scoreSubscriptionsMtx.RLock()
cp := make([]scoreSubscription, len(scoreSubscriptions))
copy(cp, scoreSubscriptions)
scoreSubscriptionsMtx.RUnlock()
for _, el := range cp {
if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) {
continue
}
if sj.User.Privileges&3 != 3 && !el.Conn.RestrictedVisible {
continue
}
el.Conn.WriteJSON(TypeNewScore, sj)
}
}
func scoreUserValid(users []subscribeScoresUser, s scoreJSON) bool {
for _, u := range users {
if u.User == s.UserID {
if len(u.Modes) > 0 {
if !inModes(u.Modes, s.PlayMode) {
return false
}
}
return true
}
}
return false
}
func inModes(modes []int, i int) bool {
for _, m := range modes {
if m == i {
return true
}
}
return false
}
func catchPanic() {
r := recover()
if r != nil {
switch r := r.(type) {
case error:
common.WSErr(r)
default:
fmt.Println("PANIC", r)
}
}
}

View File

@@ -0,0 +1,42 @@
// Package websockets implements functionality related to the API websockets.
package websockets
import (
"github.com/jmoiron/sqlx"
"gopkg.in/redis.v5"
)
var (
red *redis.Client
db *sqlx.DB
)
// Start begins websocket functionality
func Start(r *redis.Client, _db *sqlx.DB) error {
red = r
db = _db
go scoreRetriever()
go matchRetriever()
return nil
}
func cleanup(connID uint64) {
scoreSubscriptionsMtx.Lock()
for idx, el := range scoreSubscriptions {
if el.Conn.ID == connID {
scoreSubscriptions[idx] = scoreSubscriptions[len(scoreSubscriptions)-1]
scoreSubscriptions = scoreSubscriptions[:len(scoreSubscriptions)-1]
break
}
}
scoreSubscriptionsMtx.Unlock()
multiSubscriptionsMtx.Lock()
for idx, el := range multiSubscriptions {
if el.ID == connID {
multiSubscriptions[idx] = multiSubscriptions[len(multiSubscriptions)-1]
multiSubscriptions = multiSubscriptions[:len(multiSubscriptions)-1]
break
}
}
multiSubscriptionsMtx.Unlock()
}

185
beatmapget/beatmapget.go Normal file
View File

@@ -0,0 +1,185 @@
// Package beatmapget is an helper package to retrieve beatmap information from
// the osu! API, if the beatmap in the database is too old.
package beatmapget
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/osuyozora/api/common"
"github.com/jmoiron/sqlx"
"gopkg.in/thehowl/go-osuapi.v1"
)
// Expire is the duration after which a beatmap expires.
const Expire = time.Hour * 12
// DB is the database.
var DB *sqlx.DB
// Client is the osu! api client to use
var Client *osuapi.Client
// BeatmapDefiningQuality is the defining quality of the beatmap to be updated,
// which is to say either the ID, the set ID or the md5 hash.
type BeatmapDefiningQuality struct {
ID int
MD5 string
frozen bool
ranked int
}
func (b BeatmapDefiningQuality) String() string {
if b.MD5 != "" {
return "#/" + b.MD5
}
if b.ID != 0 {
return "b/" + strconv.Itoa(b.ID)
}
return "n/a"
}
func (b BeatmapDefiningQuality) isSomethingSet() error {
if b.ID == 0 && b.MD5 == "" {
return errors.New("beatmapget: at least one field in BeatmapDefiningQuality must be set")
}
return nil
}
func (b BeatmapDefiningQuality) whereAndParams() (where string, params []interface{}) {
var wheres []string
if b.ID != 0 {
wheres = append(wheres, "beatmap_id = ?")
params = append(params, b.ID)
}
if b.MD5 != "" {
wheres = append(wheres, "beatmap_md5 = ?")
params = append(params, b.MD5)
}
where = strings.Join(wheres, " AND ")
if where == "" {
where = "1"
}
return
}
// UpdateIfRequired updates the beatmap in the database if an update is required.
func UpdateIfRequired(b BeatmapDefiningQuality) error {
required, err := UpdateRequired(&b)
if err != nil && err != sql.ErrNoRows {
return err
}
if !required {
return nil
}
return Update(b, err != sql.ErrNoRows)
}
// UpdateRequired checks an update is required. If error is sql.ErrNoRows,
// it means that the beatmap should be updated, and that there was not the
// beatmap in the database previously.
func UpdateRequired(b *BeatmapDefiningQuality) (bool, error) {
if err := b.isSomethingSet(); err != nil {
return false, err
}
where, params := b.whereAndParams()
var data struct {
Difficulties [3]float64
Ranked int
Frozen bool
LatestUpdate common.UnixTimestamp
}
err := DB.QueryRow("SELECT difficulty_taiko, difficulty_ctb, difficulty_mania, ranked,"+
"ranked_status_freezed, latest_update FROM beatmaps WHERE "+
where+" LIMIT 1", params...).
Scan(
&data.Difficulties[0], &data.Difficulties[1], &data.Difficulties[2],
&data.Ranked, &data.Frozen, &data.LatestUpdate,
)
b.frozen = data.Frozen
if b.frozen {
b.ranked = data.Ranked
}
if err != nil {
if err == sql.ErrNoRows {
return true, err
}
return false, err
}
if data.Difficulties[0] == 0 && data.Difficulties[1] == 0 && data.Difficulties[2] == 0 {
return true, nil
}
expire := Expire
if data.Ranked == 2 {
expire *= 6
}
if expire != 0 && time.Now().After(time.Time(data.LatestUpdate).Add(expire)) && !data.Frozen {
return true, nil
}
return false, nil
}
// Update updates a beatmap.
func Update(b BeatmapDefiningQuality, beatmapInDB bool) error {
var data [4]osuapi.Beatmap
for i := 0; i <= 3; i++ {
mode := osuapi.Mode(i)
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
BeatmapID: b.ID,
BeatmapHash: b.MD5,
Mode: &mode,
})
if err != nil {
return err
}
if len(beatmaps) == 0 {
continue
}
data[i] = beatmaps[0]
}
var main *osuapi.Beatmap
for _, el := range data {
if el.FileMD5 != "" {
main = &el
break
}
}
if main == nil {
return fmt.Errorf("beatmapget: beatmap %s not found", b.String())
}
if beatmapInDB {
w, p := b.whereAndParams()
DB.MustExec("DELETE FROM beatmaps WHERE "+w, p...)
}
if b.frozen {
main.Approved = osuapi.ApprovedStatus(b.ranked)
}
songName := fmt.Sprintf("%s - %s [%s]", main.Artist, main.Title, main.DiffName)
_, err := DB.Exec(`INSERT INTO
beatmaps (
beatmap_id, beatmapset_id, beatmap_md5,
song_name, ar, od, difficulty_std, difficulty_taiko,
difficulty_ctb, difficulty_mania, max_combo, hit_length,
bpm, ranked, latest_update, ranked_status_freezed
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
main.BeatmapID, main.BeatmapSetID, main.FileMD5,
songName, main.ApproachRate, main.OverallDifficulty, data[0].DifficultyRating, data[1].DifficultyRating,
data[2].DifficultyRating, data[3].DifficultyRating, main.MaxCombo, main.HitLength,
int(main.BPM), main.Approved, time.Now().Unix(), b.frozen,
)
if err != nil {
return err
}
return nil
}
func init() {
osuapi.RateLimit(200)
}

81
beatmapget/fullset.go Normal file
View File

@@ -0,0 +1,81 @@
package beatmapget
import (
"database/sql"
"errors"
"time"
"github.com/osuyozora/api/common"
"gopkg.in/thehowl/go-osuapi.v1"
)
// Set checks if an update is required for all beatmaps in a set.
func Set(s int) error {
var (
lastUpdated common.UnixTimestamp
ranked int
)
err := DB.QueryRow("SELECT latest_update, ranked FROM beatmaps WHERE beatmapset_id = ? LIMIT 1", s).
Scan(&lastUpdated, &ranked)
if err != nil && err != sql.ErrNoRows {
return err
}
return set(s, lastUpdated, ranked)
}
// ErrBeatmapNotFound is returned by Beatmap if a beatmap could not be found.
var ErrBeatmapNotFound = errors.New("beatmapget: beatmap not found")
// Beatmap check if an update is required for all beatmaps in the set
// containing this beatmap.
func Beatmap(b int) (int, error) {
var (
setID int
lastUpdated common.UnixTimestamp
ranked int
)
err := DB.QueryRow("SELECT beatmapset_id, latest_update, ranked FROM beatmaps WHERE beatmap_id = ? LIMIT 1", b).
Scan(&setID, &lastUpdated, &ranked)
switch err {
case nil:
return setID, set(setID, lastUpdated, ranked)
case sql.ErrNoRows:
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
BeatmapID: b,
})
if err != nil {
return 0, err
}
if len(beatmaps) == 0 {
return 0, ErrBeatmapNotFound
}
return beatmaps[0].BeatmapSetID, set(beatmaps[0].BeatmapSetID, common.UnixTimestamp(time.Time{}), 0)
default:
return setID, err
}
}
func set(s int, updated common.UnixTimestamp, ranked int) error {
expire := Expire
if ranked == 2 {
expire *= 6
}
if time.Now().Before(time.Time(updated).Add(expire)) {
return nil
}
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
BeatmapSetID: s,
})
if err != nil {
return err
}
for _, beatmap := range beatmaps {
err := UpdateIfRequired(BeatmapDefiningQuality{
ID: beatmap.BeatmapID,
})
if err != nil {
return err
}
}
return nil
}

View File

@@ -6,6 +6,10 @@ import (
"github.com/thehowl/conf" "github.com/thehowl/conf"
) )
// Version is the git hash of the application. Do not edit. This is
// automatically set using -ldflags during build time.
var Version string
// Conf is the configuration file data for the ripple API. // Conf is the configuration file data for the ripple API.
// Conf uses https://github.com/thehowl/conf // Conf uses https://github.com/thehowl/conf
type Conf struct { type Conf struct {
@@ -13,6 +17,14 @@ type Conf struct {
DSN string `description:"The Data Source Name for the database. More: https://github.com/go-sql-driver/mysql#dsn-data-source-name"` DSN string `description:"The Data Source Name for the database. More: https://github.com/go-sql-driver/mysql#dsn-data-source-name"`
ListenTo string `description:"The IP/Port combination from which to take connections, e.g. :8080"` ListenTo string `description:"The IP/Port combination from which to take connections, e.g. :8080"`
Unix bool `description:"Bool indicating whether ListenTo is a UNIX socket or an address."` Unix bool `description:"Bool indicating whether ListenTo is a UNIX socket or an address."`
SentryDSN string `description:"thing for sentry whatever"`
HanayoKey string
BeatmapRequestsPerUser int
RankQueueSize int
OsuAPIKey string
RedisAddr string
RedisPassword string
RedisDB int
} }
var cachedConf *Conf var cachedConf *Conf
@@ -31,9 +43,23 @@ func Load() (c Conf, halt bool) {
DSN: "root@/ripple", DSN: "root@/ripple",
ListenTo: ":40001", ListenTo: ":40001",
Unix: false, Unix: false,
HanayoKey: "Potato",
BeatmapRequestsPerUser: 2,
RankQueueSize: 25,
RedisAddr: "localhost:6379",
}, "api.conf") }, "api.conf")
fmt.Println("Please compile the configuration file (api.conf).") fmt.Println("Please compile the configuration file (api.conf).")
} }
cachedConf = &c cachedConf = &c
return return
} }
// GetConf returns the cachedConf.
func GetConf() *Conf {
if cachedConf == nil {
return nil
}
// so that the cachedConf cannot actually get modified
c := *cachedConf
return &c
}

29
common/conversions.go Normal file
View File

@@ -0,0 +1,29 @@
package common
import (
"reflect"
"unsafe"
)
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
//
// Note it may break if string and/or slice header will change
// in the future go versions.
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// s2b converts string to a byte slice without memory allocation.
//
// Note it may break if string and/or slice header will change
// in the future go versions.
func s2b(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}

8
common/flags.go Normal file
View File

@@ -0,0 +1,8 @@
package common
// These are the flags an user can have. Mostly settings or things like whether
// the user has verified their email address.
const (
FlagEmailVerified = 1 << iota
FlagCountry2FA
)

View File

@@ -1,23 +1,142 @@
package common package common
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/DataDog/datadog-go/statsd"
"github.com/getsentry/raven-go"
"github.com/jmoiron/sqlx"
"github.com/valyala/fasthttp"
"gopkg.in/redis.v5"
) )
// RavenClient is the raven client to which report errors happening.
// If nil, errors will just be fmt.Println'd
var RavenClient *raven.Client
// MethodData is a struct containing the data passed over to an API method. // MethodData is a struct containing the data passed over to an API method.
type MethodData struct { type MethodData struct {
User Token User Token
DB *sql.DB DB *sqlx.DB
RequestData RequestData Doggo *statsd.Client
C *gin.Context R *redis.Client
Ctx *fasthttp.RequestCtx
} }
// Err logs an error into gin. // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
func (md MethodData) ClientIP() string {
clientIP := strings.TrimSpace(string(md.Ctx.Request.Header.Peek("X-Real-Ip")))
if len(clientIP) > 0 {
return clientIP
}
clientIP = string(md.Ctx.Request.Header.Peek("X-Forwarded-For"))
if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index]
}
clientIP = strings.TrimSpace(clientIP)
if len(clientIP) > 0 {
return clientIP
}
return md.Ctx.RemoteIP().String()
}
// Err logs an error. If RavenClient is set, it will use the client to report
// the error to sentry, otherwise it will just write the error to stdout.
func (md MethodData) Err(err error) { func (md MethodData) Err(err error) {
md.C.Error(err) user := &raven.User{
ID: strconv.Itoa(md.User.UserID),
Username: md.User.Value,
IP: md.Ctx.RemoteAddr().String(),
}
// Generate tags for error
tags := map[string]string{
"endpoint": string(md.Ctx.RequestURI()),
"token": md.User.Value,
}
_err(err, tags, user, md.Ctx)
}
// Err for peppy API calls
func Err(c *fasthttp.RequestCtx, err error) {
// Generate tags for error
tags := map[string]string{
"endpoint": string(c.RequestURI()),
}
_err(err, tags, nil, c)
}
// WSErr is the error function for errors happening in the websockets.
func WSErr(err error) {
_err(err, map[string]string{
"endpoint": "/api/v1/ws",
}, nil, nil)
}
// GenericError is just an error. Can't make a good description.
func GenericError(err error) {
_err(err, nil, nil, nil)
}
func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) {
if RavenClient == nil {
fmt.Println("ERROR!!!!")
fmt.Println(err)
return
}
// Create stacktrace
st := raven.NewStacktrace(0, 3, []string{"github.com/osuYozora", "git.github.com/osuYozora"})
ifaces := []raven.Interface{st, generateRavenHTTP(c)}
if user != nil {
ifaces = append(ifaces, user)
}
RavenClient.CaptureError(
err,
tags,
ifaces...,
)
}
func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
if ctx == nil {
return nil
}
// build uri
uri := ctx.URI()
// safe to use b2s because a new string gets allocated eventually for
// concatenation
sURI := b2s(uri.Scheme()) + "://" + b2s(uri.Host()) + b2s(uri.Path())
// build header map
// using ctx.Request.Header.Len would mean calling .VisitAll two times
// which can be quite expensive since it means iterating over all the
// headers, so we give a rough estimate of the number of headers we expect
// to have
m := make(map[string]string, 16)
ctx.Request.Header.VisitAll(func(k, v []byte) {
// not using b2s because we mustn't keep references to the underlying
// k and v
m[string(k)] = string(v)
})
return &raven.Http{
URL: sURI,
// Not using b2s because raven sending is concurrent and may happen
// AFTER the request, meaning that values could potentially be replaced
// by new ones.
Method: string(ctx.Method()),
Query: string(uri.QueryString()),
Cookies: string(ctx.Request.Header.Peek("Cookie")),
Headers: m,
}
} }
// ID retrieves the Token's owner user ID. // ID retrieves the Token's owner user ID.
@@ -25,13 +144,23 @@ func (md MethodData) ID() int {
return md.User.UserID return md.User.UserID
} }
// RequestData is the body of a request. It is wrapped into this type // Query is shorthand for md.C.Query.
// to implement the Unmarshal function, which is just a shorthand to func (md MethodData) Query(q string) string {
// json.Unmarshal. return b2s(md.Ctx.QueryArgs().Peek(q))
type RequestData []byte }
// Unmarshal json-decodes Requestdata into a value. Basically a // HasQuery returns true if the parameter is encountered in the querystring.
// shorthand to json.Unmarshal. // It returns true even if the parameter is "" (the case of ?param&etc=etc)
func (r RequestData) Unmarshal(into interface{}) error { func (md MethodData) HasQuery(q string) bool {
return json.Unmarshal([]byte(r), into) return md.Ctx.QueryArgs().Has(q)
}
// Unmarshal unmarshals a request's JSON body into an interface.
func (md MethodData) Unmarshal(into interface{}) error {
return json.Unmarshal(md.Ctx.PostBody(), into)
}
// IsBearer tells whether the current token is a Bearer (oauth) token.
func (md MethodData) IsBearer() bool {
return md.User.ID == -1
} }

View File

@@ -1,4 +0,0 @@
package common
// OsuTimeFormat is the time format for scores in the DB. Can be used with time.Parse etc.
const OsuTimeFormat = "060102150405"

View File

@@ -1,42 +1,22 @@
package common package common
import ( import "fmt"
"fmt"
"strconv"
)
// Paginate creates an additional SQL LIMIT clause for paginating. // Paginate creates an additional SQL LIMIT clause for paginating.
func Paginate(page, limit string, maxLimit int) string { func Paginate(page, limit string, maxLimit int) string {
var ( var (
pInt int p = Int(page)
lInt int l = Int(limit)
err error
) )
if page == "" { if p < 1 {
pInt = 1 p = 1
} else {
pInt, err = strconv.Atoi(page)
if err != nil {
pInt = 1
} }
if l < 1 {
l = 50
} }
if limit == "" { if l > maxLimit {
lInt = 50 l = maxLimit
} else {
lInt, err = strconv.Atoi(limit)
if err != nil {
lInt = 50
} }
} start := uint(p-1) * uint(l)
if pInt < 1 { return fmt.Sprintf(" LIMIT %d,%d ", start, l)
pInt = 1
}
if lInt < 1 {
lInt = 50
}
if lInt > maxLimit {
lInt = maxLimit
}
start := (pInt - 1) * lInt
return fmt.Sprintf(" LIMIT %d,%d ", start, lInt)
} }

49
common/paginate_test.go Normal file
View File

@@ -0,0 +1,49 @@
package common
import "testing"
func TestPaginate(t *testing.T) {
type args struct {
page string
limit string
maxLimit int
}
tests := []struct {
name string
args args
want string
}{
{
"1",
args{
"10",
"",
100,
},
" LIMIT 450,50 ",
},
{
"2",
args{
"-5",
"-15",
100,
},
" LIMIT 0,50 ",
},
{
"3",
args{
"2",
"150",
100,
},
" LIMIT 100,100 ",
},
}
for _, tt := range tests {
if got := Paginate(tt.args.page, tt.args.limit, tt.args.maxLimit); got != tt.want {
t.Errorf("%q. Paginate() = %v, want %v", tt.name, got, tt.want)
}
}
}

View File

@@ -4,7 +4,7 @@ import "strings"
// These are the various privileges a token can have. // These are the various privileges a token can have.
const ( const (
PrivilegeRead = 1 << iota // pretty much public data: leaderboard, scores, user profiles (without confidential stuff like email) PrivilegeRead = 1 << iota // used to be to fetch public data, such as user information etc. this is deprecated.
PrivilegeReadConfidential // (eventual) private messages, reports... of self PrivilegeReadConfidential // (eventual) private messages, reports... of self
PrivilegeWrite // change user information, write into confidential stuff... PrivilegeWrite // change user information, write into confidential stuff...
PrivilegeManageBadges // can change various users' badges. PrivilegeManageBadges // can change various users' badges.
@@ -22,71 +22,6 @@ const (
// Privileges is a bitwise enum of the privileges of an user's API key. // Privileges is a bitwise enum of the privileges of an user's API key.
type Privileges uint64 type Privileges uint64
// HasPrivilegeRead returns whether the Read privilege is included in the privileges.
func (p Privileges) HasPrivilegeRead() bool {
return p&PrivilegeRead != 0
}
// HasPrivilegeReadConfidential returns whether the ReadConfidential privilege is included in the privileges.
func (p Privileges) HasPrivilegeReadConfidential() bool {
return p&PrivilegeReadConfidential != 0
}
// HasPrivilegeWrite returns whether the Write privilege is included in the privileges.
func (p Privileges) HasPrivilegeWrite() bool {
return p&PrivilegeWrite != 0
}
// HasPrivilegeManageBadges returns whether the ManageBadges privilege is included in the privileges.
func (p Privileges) HasPrivilegeManageBadges() bool {
return p&PrivilegeManageBadges != 0
}
// HasPrivilegeBetaKeys returns whether the BetaKeys privilege is included in the privileges.
func (p Privileges) HasPrivilegeBetaKeys() bool {
return p&PrivilegeBetaKeys != 0
}
// HasPrivilegeManageSettings returns whether the ManageSettings privilege is included in the privileges.
func (p Privileges) HasPrivilegeManageSettings() bool {
return p&PrivilegeManageSettings != 0
}
// HasPrivilegeViewUserAdvanced returns whether the ViewUserAdvanced privilege is included in the privileges.
func (p Privileges) HasPrivilegeViewUserAdvanced() bool {
return p&PrivilegeViewUserAdvanced != 0
}
// HasPrivilegeManageUser returns whether the ManageUser privilege is included in the privileges.
func (p Privileges) HasPrivilegeManageUser() bool {
return p&PrivilegeManageUser != 0
}
// HasPrivilegeManageRoles returns whether the ManageRoles privilege is included in the privileges.
func (p Privileges) HasPrivilegeManageRoles() bool {
return p&PrivilegeManageRoles != 0
}
// HasPrivilegeManageAPIKeys returns whether the ManageAPIKeys privilege is included in the privileges.
func (p Privileges) HasPrivilegeManageAPIKeys() bool {
return p&PrivilegeManageAPIKeys != 0
}
// HasPrivilegeBlog returns whether the Blog privilege is included in the privileges.
func (p Privileges) HasPrivilegeBlog() bool {
return p&PrivilegeBlog != 0
}
// HasPrivilegeAPIMeta returns whether the APIMeta privilege is included in the privileges.
func (p Privileges) HasPrivilegeAPIMeta() bool {
return p&PrivilegeAPIMeta != 0
}
// HasPrivilegeBeatmap returns whether the Beatmap privilege is included in the privileges.
func (p Privileges) HasPrivilegeBeatmap() bool {
return p&PrivilegeBeatmap != 0
}
var privilegeString = [...]string{ var privilegeString = [...]string{
"Read", "Read",
"ReadConfidential", "ReadConfidential",
@@ -106,35 +41,35 @@ var privilegeString = [...]string{
func (p Privileges) String() string { func (p Privileges) String() string {
var pvs []string var pvs []string
for i, v := range privilegeString { for i, v := range privilegeString {
if int(p)&(1<<uint(i)) != 0 { if uint64(p)&uint64(1<<uint(i)) != 0 {
pvs = append(pvs, v) pvs = append(pvs, v)
} }
} }
return strings.Join(pvs, ", ") return strings.Join(pvs, ", ")
} }
var privilegeMustBe = [...]int{ var privilegeMustBe = [...]UserPrivileges{
1, 1 << 30, // read is deprecated, and should be given out to no-one.
1, UserPrivilegeNormal,
1, UserPrivilegeNormal,
3, AdminPrivilegeAccessRAP | AdminPrivilegeManageBadges,
3, AdminPrivilegeAccessRAP | AdminPrivilegeManageBetaKey,
4, AdminPrivilegeAccessRAP | AdminPrivilegeManageSetting,
4, AdminPrivilegeAccessRAP,
4, AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeBanUsers,
4, AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManagePrivilege,
4, AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManageServer,
3, AdminPrivilegeChatMod, // temporary?
4, AdminPrivilegeManageServer,
4, AdminPrivilegeAccessRAP | AdminPrivilegeManageBeatmap,
} }
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank. // CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
func (p Privileges) CanOnly(rank int) Privileges { func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
newPrivilege := 0 newPrivilege := 0
for i, v := range privilegeMustBe { for i, v := range privilegeMustBe {
wants := p&1 == 1 wants := p&1 == 1
can := rank >= v can := userPrivs&v == v
if wants && can { if wants && can {
newPrivilege |= 1 << uint(i) newPrivilege |= 1 << uint(i)
} }
@@ -142,3 +77,18 @@ func (p Privileges) CanOnly(rank int) Privileges {
} }
return Privileges(newPrivilege) return Privileges(newPrivilege)
} }
var privilegeMap = map[string]Privileges{
"read_confidential": PrivilegeReadConfidential,
"write": PrivilegeWrite,
}
// OAuthPrivileges returns the equivalent in Privileges of a space-separated
// list of scopes.
func OAuthPrivileges(scopes string) Privileges {
var p Privileges
for _, x := range strings.Split(scopes, " ") {
p |= privilegeMap[x]
}
return p
}

16
common/sanitisation.go Normal file
View File

@@ -0,0 +1,16 @@
package common
import (
"unicode"
)
// SanitiseString removes all control codes from a string.
func SanitiseString(s string) string {
n := make([]rune, 0, len(s))
for _, c := range s {
if c == '\n' || !unicode.Is(unicode.Other, c) {
n = append(n, c)
}
}
return string(n)
}

View File

@@ -0,0 +1,41 @@
package common
import "testing"
const pen = "I trattori di palmizio 나는 펜이있다. 私はリンゴを持っています。" +
"啊! 苹果笔。 у меня есть ручка, Tôi có dứa. අන්නාසි පෑන"
func TestSanitiseString(t *testing.T) {
tests := []struct {
name string
arg string
want string
}{
{
"Normal",
pen,
pen,
},
{
"Arabic (rtl)",
"أناناس",
"أناناس",
},
{
"Null",
"A\x00B",
"AB",
},
}
for _, tt := range tests {
if got := SanitiseString(tt.arg); got != tt.want {
t.Errorf("%q. SanitiseString() = %v, want %v", tt.name, got, tt.want)
}
}
}
func BenchmarkSanitiseString(b *testing.B) {
for i := 0; i < b.N; i++ {
SanitiseString(pen)
}
}

52
common/sort.go Normal file
View File

@@ -0,0 +1,52 @@
package common
import "strings"
// SortConfiguration is the configuration of Sort.
type SortConfiguration struct {
Allowed []string // Allowed parameters
Default string
DefaultSorting string // if empty, DESC
Table string
}
// Sort allows the request to modify how the query is sorted.
func Sort(md MethodData, config SortConfiguration) string {
if config.DefaultSorting == "" {
config.DefaultSorting = "DESC"
}
if config.Table != "" {
config.Table += "."
}
var sortBy string
for _, s := range md.Ctx.Request.URI().QueryArgs().PeekMulti("sort") {
sortParts := strings.Split(strings.ToLower(b2s(s)), ",")
if contains(config.Allowed, sortParts[0]) {
if sortBy != "" {
sortBy += ", "
}
sortBy += config.Table + sortParts[0] + " "
if len(sortParts) > 1 && contains([]string{"asc", "desc"}, sortParts[1]) {
sortBy += sortParts[1]
} else {
sortBy += config.DefaultSorting
}
}
}
if sortBy == "" {
sortBy = config.Default
}
if sortBy == "" {
return ""
}
return "ORDER BY " + sortBy
}
func contains(a []string, s string) bool {
for _, el := range a {
if s == el {
return true
}
}
return false
}

View File

@@ -1,9 +1,23 @@
package common package common
// Token Is an API token. import "fmt"
// Token is an API token.
type Token struct { type Token struct {
ID int ID int
Value string Value string
UserID int UserID int
Privileges Privileges TokenPrivileges Privileges
UserPrivileges UserPrivileges
}
// OnlyUserPublic returns a string containing "(user.privileges & 1 = 1 OR users.id = <userID>)"
// if the user does not have the UserPrivilege AdminManageUsers, and returns "1" otherwise.
func (t Token) OnlyUserPublic(userManagerSeesEverything bool) string {
if userManagerSeesEverything &&
t.UserPrivileges&AdminPrivilegeManageUsers == AdminPrivilegeManageUsers {
return "1"
}
// It's safe to use sprintf directly even if it's a query, because UserID is an int.
return fmt.Sprintf("(users.privileges & 1 = 1 OR users.id = '%d')", t.UserID)
} }

55
common/unix_timestamp.go Normal file
View File

@@ -0,0 +1,55 @@
package common
import (
"errors"
"strconv"
"time"
)
// UnixTimestamp is simply a time.Time, but can be used to convert an
// unix timestamp in the database into a native time.Time.
type UnixTimestamp time.Time
// Scan decodes src into an unix timestamp.
func (u *UnixTimestamp) Scan(src interface{}) error {
if u == nil {
return errors.New("rippleapi/common: UnixTimestamp is nil")
}
switch src := src.(type) {
case int64:
*u = UnixTimestamp(time.Unix(src, 0))
case float64:
*u = UnixTimestamp(time.Unix(int64(src), 0))
case string:
return u._string(src)
case []byte:
return u._string(string(src))
case nil:
// Nothing, leave zero value on timestamp
default:
return errors.New("rippleapi/common: unhandleable type")
}
return nil
}
func (u *UnixTimestamp) _string(s string) error {
ts, err := strconv.Atoi(s)
if err != nil {
return err
}
*u = UnixTimestamp(time.Unix(int64(ts), 0))
return nil
}
// MarshalJSON -> time.Time.MarshalJSON
func (u UnixTimestamp) MarshalJSON() ([]byte, error) {
return time.Time(u).MarshalJSON()
}
// UnmarshalJSON -> time.Time.UnmarshalJSON
func (u *UnixTimestamp) UnmarshalJSON(x []byte) error {
t := new(time.Time)
err := t.UnmarshalJSON(x)
*u = UnixTimestamp(*t)
return err
}

32
common/update.go Normal file
View File

@@ -0,0 +1,32 @@
package common
import (
"reflect"
"strings"
)
// UpdateQuery is simply an SQL update query,
// that can be built upon passed parameters.
type UpdateQuery struct {
fields []string
Parameters []interface{}
}
// Add adds a new field with correspective value to UpdateQuery
func (u *UpdateQuery) Add(field string, value interface{}) *UpdateQuery {
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr && val.IsNil() {
return u
}
if s, ok := value.(string); ok && s == "" {
return u
}
u.fields = append(u.fields, field+" = ?")
u.Parameters = append(u.Parameters, value)
return u
}
// Fields retrieves the fields joined by a comma.
func (u *UpdateQuery) Fields() string {
return strings.Join(u.fields, ", ")
}

68
common/user_privileges.go Normal file
View File

@@ -0,0 +1,68 @@
package common
import "strings"
// user/admin privileges
const (
UserPrivilegePublic UserPrivileges = 1 << iota
UserPrivilegeNormal
UserPrivilegeDonor
AdminPrivilegeAccessRAP
AdminPrivilegeManageUsers
AdminPrivilegeBanUsers
AdminPrivilegeSilenceUsers
AdminPrivilegeWipeUsers
AdminPrivilegeManageBeatmap
AdminPrivilegeManageServer
AdminPrivilegeManageSetting
AdminPrivilegeManageBetaKey
AdminPrivilegeManageReport
AdminPrivilegeManageDocs
AdminPrivilegeManageBadges
AdminPrivilegeViewRAPLogs
AdminPrivilegeManagePrivilege
AdminPrivilegeSendAlerts
AdminPrivilegeChatMod
AdminPrivilegeKickUsers
UserPrivilegePendingVerification
UserPrivilegeTournamentStaff
AdminPrivilegeCaker
)
// UserPrivileges represents a bitwise enum of the privileges of an user.
type UserPrivileges uint64
var userPrivilegeString = [...]string{
"UserPublic",
"UserNormal",
"UserDonor",
"AdminAccessRAP",
"AdminManageUsers",
"AdminBanUsers",
"AdminSilenceUsers",
"AdminWipeUsers",
"AdminManageBeatmap",
"AdminManageServer",
"AdminManageSetting",
"AdminManageBetaKey",
"AdminManageReport",
"AdminManageDocs",
"AdminManageBadges",
"AdminViewRAPLogs",
"AdminManagePrivilege",
"AdminSendAlerts",
"AdminChatMod",
"AdminKickUsers",
"UserPendingVerification",
"UserTournamentStaff",
}
func (p UserPrivileges) String() string {
var pvs []string
for i, v := range userPrivilegeString {
if uint64(p)&uint64(1<<uint(i)) != 0 {
pvs = append(pvs, v)
}
}
return strings.Join(pvs, ", ")
}

11
common/utils.go Normal file
View File

@@ -0,0 +1,11 @@
package common
import (
"strings"
)
// SafeUsername makes a string lowercase and replaces all spaces with
// underscores.
func SafeUsername(s string) string {
return strings.Replace(strings.ToLower(s), " ", "_", -1)
}

20
common/utils_test.go Normal file
View File

@@ -0,0 +1,20 @@
package common
import "testing"
func TestSafeUsername(t *testing.T) {
tests := []struct {
name string
arg string
want string
}{
{"noChange", "no_change", "no_change"},
{"toLower", "Change_Me", "change_me"},
{"complete", "La_M a m m a_putt na", "la_m_a_m_m_a_putt_na"},
}
for _, tt := range tests {
if got := SafeUsername(tt.arg); got != tt.want {
t.Errorf("%q. SafeUsername() = %v, want %v", tt.name, got, tt.want)
}
}
}

91
common/where.go Normal file
View File

@@ -0,0 +1,91 @@
package common
// WhereClause is a struct representing a where clause.
// This is made to easily create WHERE clauses from parameters passed from a request.
type WhereClause struct {
Clause string
Params []interface{}
useOr bool
}
// Where adds a new WHERE clause to the WhereClause.
func (w *WhereClause) Where(clause, passedParam string, allowedValues ...string) *WhereClause {
if passedParam == "" {
return w
}
if len(allowedValues) != 0 && !contains(allowedValues, passedParam) {
return w
}
w.addWhere()
w.Clause += clause
w.Params = append(w.Params, passedParam)
return w
}
func (w *WhereClause) addWhere() {
// if string is empty add "WHERE", else add AND
if w.Clause == "" {
w.Clause += "WHERE "
} else {
if w.useOr {
w.Clause += " OR "
return
}
w.Clause += " AND "
}
}
// Or enables using OR instead of AND
func (w *WhereClause) Or() *WhereClause {
w.useOr = true
return w
}
// And enables using AND instead of OR
func (w *WhereClause) And() *WhereClause {
w.useOr = false
return w
}
// In generates an IN clause.
// initial is the initial part, e.g. "users.id".
// Fields are the possible values.
// Sample output: users.id IN ('1', '2', '3')
func (w *WhereClause) In(initial string, fields ...[]byte) *WhereClause {
if len(fields) == 0 {
return w
}
w.addWhere()
w.Clause += initial + " IN (" + generateQuestionMarks(len(fields)) + ")"
fieldsInterfaced := make([]interface{}, len(fields))
for k, f := range fields {
fieldsInterfaced[k] = string(f)
}
w.Params = append(w.Params, fieldsInterfaced...)
return w
}
func generateQuestionMarks(x int) (qm string) {
for i := 0; i < x-1; i++ {
qm += "?, "
}
if x > 0 {
qm += "?"
}
return qm
}
// ClauseSafe returns the clause, always containing something. If w.Clause is
// empty, it returns "WHERE 1".
func (w *WhereClause) ClauseSafe() string {
if w.Clause == "" {
return "WHERE 1"
}
return w.Clause
}
// Where is the same as WhereClause.Where, but creates a new WhereClause.
func Where(clause, passedParam string, allowedValues ...string) *WhereClause {
w := new(WhereClause)
return w.Where(clause, passedParam, allowedValues...)
}

97
common/where_test.go Normal file
View File

@@ -0,0 +1,97 @@
package common
import (
"reflect"
"testing"
)
func Test_generateQuestionMarks(t *testing.T) {
type args struct {
x int
}
tests := []struct {
name string
args args
wantQm string
}{
{"-1", args{-1}, ""},
{"0", args{0}, ""},
{"1", args{1}, "?"},
{"2", args{2}, "?, ?"},
}
for _, tt := range tests {
if gotQm := generateQuestionMarks(tt.args.x); gotQm != tt.wantQm {
t.Errorf("%q. generateQuestionMarks() = %v, want %v", tt.name, gotQm, tt.wantQm)
}
}
}
func TestWhereClause_In(t *testing.T) {
type args struct {
initial string
fields []string
}
tests := []struct {
name string
fields *WhereClause
args args
want *WhereClause
}{
{
"simple",
&WhereClause{},
args{"users.id", []string{"1", "2", "3"}},
&WhereClause{"WHERE users.id IN (?, ?, ?)", []interface{}{"1", "2", "3"}, false},
},
{
"withExisting",
Where("users.username = ?", "Howl").Where("users.xd > ?", "6"),
args{"users.id", []string{"1"}},
&WhereClause{
"WHERE users.username = ? AND users.xd > ? AND users.id IN (?)",
[]interface{}{"Howl", "6", "1"},
false,
},
},
}
for _, tt := range tests {
w := tt.fields
if got := w.In(tt.args.initial, tt.args.fields...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. WhereClause.In() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestWhere(t *testing.T) {
type args struct {
clause string
passedParam string
allowedValues []string
}
tests := []struct {
name string
args args
want *WhereClause
}{
{
"simple",
args{"users.id = ?", "5", nil},
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
},
{
"allowed",
args{"users.id = ?", "5", []string{"1", "3", "5"}},
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
},
{
"notAllowed",
args{"users.id = ?", "5", []string{"0"}},
&WhereClause{},
},
}
for _, tt := range tests {
if got := Where(tt.args.clause, tt.args.passedParam, tt.args.allowedValues...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Where() = %#v, want %#v", tt.name, got, tt.want)
}
}
}

122
limit/limit.go Normal file
View File

@@ -0,0 +1,122 @@
package limit
import (
"fmt"
"sync"
"time"
)
// Request is a Request with DefaultLimiter.
func Request(u string, perMinute int) { DefaultLimiter.Request(u, perMinute) }
// NonBlockingRequest is a NonBlockingRequest with DefaultLimiter.
func NonBlockingRequest(u string, perMinute int) bool {
return DefaultLimiter.NonBlockingRequest(u, perMinute)
}
// DefaultLimiter is the RateLimiter used by the package-level
// Request and NonBlockingRequest.
var DefaultLimiter = &RateLimiter{
Map: make(map[string]chan struct{}),
Mutex: &sync.RWMutex{},
}
// RateLimiter is a simple rate limiter.
type RateLimiter struct {
Map map[string]chan struct{}
Mutex *sync.RWMutex
}
// Request is a simple request. Blocks until it can make the request.
func (s *RateLimiter) Request(u string, perMinute int) {
s.request(u, perMinute, true)
}
// NonBlockingRequest checks if it can do a request. If it can't, it returns
// false, else it returns true if the request succeded.
func (s *RateLimiter) NonBlockingRequest(u string, perMinute int) bool {
return s.request(u, perMinute, false)
}
func (s *RateLimiter) request(u string, perMinute int, blocking bool) bool {
s.check()
s.Mutex.RLock()
c, exists := s.Map[u]
s.Mutex.RUnlock()
if !exists {
c = makePrefilledChan(perMinute)
s.Mutex.Lock()
// Now that we have exclusive read and write-access, we want to
// make sure we don't overwrite an existing channel. Otherwise,
// race conditions and panic happen.
if cNew, exists := s.Map[u]; exists {
c = cNew
s.Mutex.Unlock()
} else {
s.Map[u] = c
s.Mutex.Unlock()
<-c
go s.filler(u, perMinute)
}
}
return rcv(c, blocking)
}
// rcv receives from a channel, but if blocking is true it waits til something
// is received and always returns true, otherwise if it can't receive it
// returns false.
func rcv(c chan struct{}, blocking bool) bool {
if blocking {
<-c
return true
}
select {
case <-c:
return true
default:
return false
}
}
func (s *RateLimiter) filler(el string, perMinute int) {
defer func() {
r := recover()
if r != nil {
fmt.Println(r)
}
}()
s.Mutex.RLock()
c := s.Map[el]
s.Mutex.RUnlock()
for {
select {
case c <- struct{}{}:
time.Sleep(time.Minute / time.Duration(perMinute))
default: // c is full
s.Mutex.Lock()
close(c)
delete(s.Map, el)
s.Mutex.Unlock()
return
}
}
}
// check makes sure the map and the mutex are properly initialised.
func (s *RateLimiter) check() {
if s.Map == nil {
s.Map = make(map[string]chan struct{})
}
if s.Mutex == nil {
s.Mutex = new(sync.RWMutex)
}
}
func makePrefilledChan(l int) chan struct{} {
c := make(chan struct{}, l)
for i := 0; i < l; i++ {
c <- struct{}{}
}
return c
}

120
main.go
View File

@@ -1,28 +1,47 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"net" "strings"
"net/http"
"syscall" "syscall"
"time"
"git.zxq.co/ripple/rippleapi/app" "zxq.co/ripple/agplwarning"
"git.zxq.co/ripple/rippleapi/common" "github.com/osuyozora/api/app"
"git.zxq.co/ripple/schiavolib" "github.com/osuyozora/api/beatmapget"
"github.com/rcrowley/goagain" "github.com/osuyozora/api/common"
"zxq.co/ripple/schiavolib"
// Golint pls dont break balls // Golint pls dont break balls
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/serenize/snaker"
"gopkg.in/thehowl/go-osuapi.v1"
) )
// Version is the git hash of the application. Do not edit. This is
// automatically set using -ldflags during build time.
var Version string
func init() { func init() {
log.SetFlags(log.Ltime) log.SetFlags(log.Ltime)
log.SetPrefix(fmt.Sprintf("%d|", syscall.Getpid())) log.SetPrefix(fmt.Sprintf("%d|", syscall.Getpid()))
common.Version = Version
} }
var db *sqlx.DB
func main() { func main() {
err := agplwarning.Warn("ripple", "Ripple API")
if err != nil {
fmt.Println(err)
}
fmt.Print("Ripple API")
if Version != "" {
fmt.Print("; git commit hash: ", Version)
}
fmt.Println()
conf, halt := common.Load() conf, halt := common.Load()
if halt { if halt {
return return
@@ -30,65 +49,44 @@ func main() {
schiavo.Prefix = "Ripple API" schiavo.Prefix = "Ripple API"
db, err := sql.Open(conf.DatabaseType, conf.DSN) if !strings.Contains(conf.DSN, "parseTime=true") {
c := "?"
if strings.Contains(conf.DSN, "?") {
c = "&"
}
conf.DSN += c + "parseTime=true&charset=utf8mb4,utf8&collation=utf8mb4_general_ci"
}
db, err = sqlx.Open(conf.DatabaseType, conf.DSN)
if err != nil { if err != nil {
schiavo.Bunker.Send(err.Error()) schiavo.Bunker.Send(err.Error())
log.Fatalln(err) log.Fatalln(err)
} }
db.MapperFunc(func(s string) string {
if x, ok := commonClusterfucks[s]; ok {
return x
}
return snaker.CamelToSnake(s)
})
beatmapget.Client = osuapi.NewClient(conf.OsuAPIKey)
beatmapget.DB = db
engine := app.Start(conf, db) engine := app.Start(conf, db)
// Inherit a net.Listener from our parent process or listen anew. startuato(engine.Handler)
l, err := goagain.Listener()
if nil != err {
// Listen on a TCP or a UNIX domain socket (TCP here).
if conf.Unix {
l, err = net.Listen("unix", conf.ListenTo)
} else {
l, err = net.Listen("tcp", conf.ListenTo)
}
if nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
} }
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr())) var commonClusterfucks = map[string]string{
"RegisteredOn": "register_datetime",
// Accept connections in a new goroutine. "UsernameAKA": "username_aka",
go http.Serve(l, engine) "BeatmapMD5": "beatmap_md5",
"Count300": "300_count",
} else { "Count100": "100_count",
"Count50": "50_count",
// Resume accepting connections in a new goroutine. "CountGeki": "gekis_count",
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr())) "CountKatu": "katus_count",
go http.Serve(l, engine) "CountMiss": "misses_count",
"PP": "pp",
// Kill the parent, now that the child has started successfully.
if err := goagain.Kill(); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
}
// Block the main goroutine awaiting signals.
if _, err := goagain.Wait(l); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
// Do whatever's necessary to ensure a graceful exit like waiting for
// goroutines to terminate or a channel to become closed.
//
// In this case, we'll simply stop listening and wait one second.
if err := l.Close(); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
if err := db.Close(); err != nil {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
time.Sleep(time.Second * 1)
} }

71
startuato_linux.go Normal file
View File

@@ -0,0 +1,71 @@
// +build !windows
package main
import (
"fmt"
"log"
"net"
"time"
"github.com/rcrowley/goagain"
"github.com/valyala/fasthttp"
"github.com/osuyozora/api/common"
"zxq.co/ripple/schiavolib"
)
func startuato(hn fasthttp.RequestHandler) {
conf, _ := common.Load()
// Inherit a net.Listener from our parent process or listen anew.
l, err := goagain.Listener()
if nil != err {
// Listen on a TCP or a UNIX domain socket (TCP here).
if conf.Unix {
l, err = net.Listen("unix", conf.ListenTo)
} else {
l, err = net.Listen("tcp", conf.ListenTo)
}
if nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr()))
// Accept connections in a new goroutine.
go fasthttp.Serve(l, hn)
} else {
// Resume accepting connections in a new goroutine.
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr()))
go fasthttp.Serve(l, hn)
// Kill the parent, now that the child has started successfully.
if err := goagain.Kill(); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
}
// Block the main goroutine awaiting signals.
if _, err := goagain.Wait(l); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
// Do whatever's necessary to ensure a graceful exit like waiting for
// goroutines to terminate or a channel to become closed.
//
// In this case, we'll simply stop listening and wait one second.
if err := l.Close(); nil != err {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
if err := db.Close(); err != nil {
schiavo.Bunker.Send(err.Error())
log.Fatalln(err)
}
time.Sleep(time.Second * 1)
}

34
startuato_windows.go Normal file
View File

@@ -0,0 +1,34 @@
// +build windows
// The Ripple API on Windows is not officially supported and you're probably
// gonna swear a lot if you intend to use it on Windows. Caveat emptor.
package main
import (
"log"
"net"
"github.com/valyala/fasthttp"
"github.com/osuyozora/api/common"
)
func startuato(hn fasthttp.RequestHandler) {
conf, _ := common.Load()
var (
l net.Listener
err error
)
// Listen on a TCP or a UNIX domain socket.
if conf.Unix {
l, err = net.Listen("unix", conf.ListenTo)
} else {
l, err = net.Listen("tcp", conf.ListenTo)
}
if nil != err {
log.Fatalln(err)
}
fasthttp.Serve(l, hn)
}

19
vendor/github.com/DataDog/datadog-go/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2015 Datadog, Inc
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.

52
vendor/github.com/DataDog/datadog-go/statsd/README.md generated vendored Normal file
View File

@@ -0,0 +1,52 @@
## Overview
Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags
and histograms.
## Get the code
$ go get github.com/DataDog/datadog-go/statsd
## Usage
```go
// Create the client
c, err := statsd.New("127.0.0.1:8125")
if err != nil {
log.Fatal(err)
}
// Prefix every metric with the app name
c.Namespace = "flubber."
// Send the EC2 availability zone as a tag with every metric
c.Tags = append(c.Tags, "us-east-1a")
// Do some metrics!
err = c.Gauge("request.queue_depth", 12, nil, 1)
err = c.Timing("request.duration", duration, nil, 1) // Uses a time.Duration!
err = c.TimeInMilliseconds("request", 12, nil, 1)
err = c.Incr("request.count_total", nil, 1)
err = c.Decr("request.count_total", nil, 1)
err = c.Count("request.count_total", 2, nil, 1)
```
## Buffering Client
DogStatsD accepts packets with multiple statsd payloads in them. Using the BufferingClient via `NewBufferingClient` will buffer up commands and send them when the buffer is reached or after 100msec.
## Development
Run the tests with:
$ go test
## Documentation
Please see: http://godoc.org/github.com/DataDog/datadog-go/statsd
## License
go-dogstatsd is released under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
## Credits
Original code by [ooyala](https://github.com/ooyala/go-dogstatsd).

577
vendor/github.com/DataDog/datadog-go/statsd/statsd.go generated vendored Normal file
View File

@@ -0,0 +1,577 @@
// Copyright 2013 Ooyala, Inc.
/*
Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd,
adding tags and histograms and pushing upstream to Datadog.
Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD.
Example Usage:
// Create the client
c, err := statsd.New("127.0.0.1:8125")
if err != nil {
log.Fatal(err)
}
// Prefix every metric with the app name
c.Namespace = "flubber."
// Send the EC2 availability zone as a tag with every metric
c.Tags = append(c.Tags, "us-east-1a")
err = c.Gauge("request.duration", 1.2, nil, 1)
statsd is based on go-statsd-client.
*/
package statsd
import (
"bytes"
"errors"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
/*
OptimalPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes
is optimal for regular networks with an MTU of 1500 so datagrams don't get
fragmented. It's generally recommended not to fragment UDP datagrams as losing
a single fragment will cause the entire datagram to be lost.
This can be increased if your network has a greater MTU or you don't mind UDP
datagrams getting fragmented. The practical limit is MaxUDPPayloadSize
*/
const OptimalPayloadSize = 1432
/*
MaxUDPPayloadSize defines the maximum payload size for a UDP datagram.
Its value comes from the calculation: 65535 bytes Max UDP datagram size -
8byte UDP header - 60byte max IP headers
any number greater than that will see frames being cut out.
*/
const MaxUDPPayloadSize = 65467
// A Client is a handle for sending udp messages to dogstatsd. It is safe to
// use one Client from multiple goroutines simultaneously.
type Client struct {
conn net.Conn
// Namespace to prepend to all statsd calls
Namespace string
// Tags are global tags to be added to every statsd call
Tags []string
// BufferLength is the length of the buffer in commands.
bufferLength int
flushTime time.Duration
commands []string
buffer bytes.Buffer
stop bool
sync.Mutex
}
// New returns a pointer to a new Client given an addr in the format "hostname:port".
func New(addr string) (*Client, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
client := &Client{conn: conn}
return client, nil
}
// NewBuffered returns a Client that buffers its output and sends it in chunks.
// Buflen is the length of the buffer in number of commands.
func NewBuffered(addr string, buflen int) (*Client, error) {
client, err := New(addr)
if err != nil {
return nil, err
}
client.bufferLength = buflen
client.commands = make([]string, 0, buflen)
client.flushTime = time.Millisecond * 100
go client.watch()
return client, nil
}
// format a message from its name, value, tags and rate. Also adds global
// namespace and tags.
func (c *Client) format(name, value string, tags []string, rate float64) string {
var buf bytes.Buffer
if c.Namespace != "" {
buf.WriteString(c.Namespace)
}
buf.WriteString(name)
buf.WriteString(":")
buf.WriteString(value)
if rate < 1 {
buf.WriteString(`|@`)
buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64))
}
writeTagString(&buf, c.Tags, tags)
return buf.String()
}
func (c *Client) watch() {
for _ = range time.Tick(c.flushTime) {
if c.stop {
return
}
c.Lock()
if len(c.commands) > 0 {
// FIXME: eating error here
c.flush()
}
c.Unlock()
}
}
func (c *Client) append(cmd string) error {
c.Lock()
defer c.Unlock()
c.commands = append(c.commands, cmd)
// if we should flush, lets do it
if len(c.commands) == c.bufferLength {
if err := c.flush(); err != nil {
return err
}
}
return nil
}
func (c *Client) joinMaxSize(cmds []string, sep string, maxSize int) ([][]byte, []int) {
c.buffer.Reset() //clear buffer
var frames [][]byte
var ncmds []int
sepBytes := []byte(sep)
sepLen := len(sep)
elem := 0
for _, cmd := range cmds {
needed := len(cmd)
if elem != 0 {
needed = needed + sepLen
}
if c.buffer.Len()+needed <= maxSize {
if elem != 0 {
c.buffer.Write(sepBytes)
}
c.buffer.WriteString(cmd)
elem++
} else {
frames = append(frames, copyAndResetBuffer(&c.buffer))
ncmds = append(ncmds, elem)
// if cmd is bigger than maxSize it will get flushed on next loop
c.buffer.WriteString(cmd)
elem = 1
}
}
//add whatever is left! if there's actually something
if c.buffer.Len() > 0 {
frames = append(frames, copyAndResetBuffer(&c.buffer))
ncmds = append(ncmds, elem)
}
return frames, ncmds
}
func copyAndResetBuffer(buf *bytes.Buffer) []byte {
tmpBuf := make([]byte, buf.Len())
copy(tmpBuf, buf.Bytes())
buf.Reset()
return tmpBuf
}
// flush the commands in the buffer. Lock must be held by caller.
func (c *Client) flush() error {
frames, flushable := c.joinMaxSize(c.commands, "\n", OptimalPayloadSize)
var err error
cmdsFlushed := 0
for i, data := range frames {
_, e := c.conn.Write(data)
if e != nil {
err = e
break
}
cmdsFlushed += flushable[i]
}
// clear the slice with a slice op, doesn't realloc
if cmdsFlushed == len(c.commands) {
c.commands = c.commands[:0]
} else {
//this case will cause a future realloc...
// drop problematic command though (sorry).
c.commands = c.commands[cmdsFlushed+1:]
}
return err
}
func (c *Client) sendMsg(msg string) error {
// return an error if message is bigger than MaxUDPPayloadSize
if len(msg) > MaxUDPPayloadSize {
return errors.New("message size exceeds MaxUDPPayloadSize")
}
// if this client is buffered, then we'll just append this
if c.bufferLength > 0 {
return c.append(msg)
}
_, err := c.conn.Write([]byte(msg))
return err
}
// send handles sampling and sends the message over UDP. It also adds global namespace prefixes and tags.
func (c *Client) send(name, value string, tags []string, rate float64) error {
if c == nil {
return nil
}
if rate < 1 && rand.Float64() > rate {
return nil
}
data := c.format(name, value, tags, rate)
return c.sendMsg(data)
}
// Gauge measures the value of a metric at a particular time.
func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error {
stat := fmt.Sprintf("%f|g", value)
return c.send(name, stat, tags, rate)
}
// Count tracks how many times something happened per second.
func (c *Client) Count(name string, value int64, tags []string, rate float64) error {
stat := fmt.Sprintf("%d|c", value)
return c.send(name, stat, tags, rate)
}
// Histogram tracks the statistical distribution of a set of values.
func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error {
stat := fmt.Sprintf("%f|h", value)
return c.send(name, stat, tags, rate)
}
// Decr is just Count of 1
func (c *Client) Decr(name string, tags []string, rate float64) error {
return c.send(name, "-1|c", tags, rate)
}
// Incr is just Count of 1
func (c *Client) Incr(name string, tags []string, rate float64) error {
return c.send(name, "1|c", tags, rate)
}
// Set counts the number of unique elements in a group.
func (c *Client) Set(name string, value string, tags []string, rate float64) error {
stat := fmt.Sprintf("%s|s", value)
return c.send(name, stat, tags, rate)
}
// Timing sends timing information, it is an alias for TimeInMilliseconds
func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error {
return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate)
}
// TimeInMilliseconds sends timing information in milliseconds.
// It is flushed by statsd with percentiles, mean and other info (.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
stat := fmt.Sprintf("%f|ms", value)
return c.send(name, stat, tags, rate)
}
// Event sends the provided Event.
func (c *Client) Event(e *Event) error {
stat, err := e.Encode(c.Tags...)
if err != nil {
return err
}
return c.sendMsg(stat)
}
// SimpleEvent sends an event with the provided title and text.
func (c *Client) SimpleEvent(title, text string) error {
e := NewEvent(title, text)
return c.Event(e)
}
// ServiceCheck sends the provided ServiceCheck.
func (c *Client) ServiceCheck(sc *ServiceCheck) error {
stat, err := sc.Encode(c.Tags...)
if err != nil {
return err
}
return c.sendMsg(stat)
}
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
sc := NewServiceCheck(name, status)
return c.ServiceCheck(sc)
}
// Close the client connection.
func (c *Client) Close() error {
if c == nil {
return nil
}
c.stop = true
return c.conn.Close()
}
// Events support
type eventAlertType string
const (
// Info is the "info" AlertType for events
Info eventAlertType = "info"
// Error is the "error" AlertType for events
Error eventAlertType = "error"
// Warning is the "warning" AlertType for events
Warning eventAlertType = "warning"
// Success is the "success" AlertType for events
Success eventAlertType = "success"
)
type eventPriority string
const (
// Normal is the "normal" Priority for events
Normal eventPriority = "normal"
// Low is the "low" Priority for events
Low eventPriority = "low"
)
// An Event is an object that can be posted to your DataDog event stream.
type Event struct {
// Title of the event. Required.
Title string
// Text is the description of the event. Required.
Text string
// Timestamp is a timestamp for the event. If not provided, the dogstatsd
// server will set this to the current time.
Timestamp time.Time
// Hostname for the event.
Hostname string
// AggregationKey groups this event with others of the same key.
AggregationKey string
// Priority of the event. Can be statsd.Low or statsd.Normal.
Priority eventPriority
// SourceTypeName is a source type for the event.
SourceTypeName string
// AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success.
// If absent, the default value applied by the dogstatsd server is Info.
AlertType eventAlertType
// Tags for the event.
Tags []string
}
// NewEvent creates a new event with the given title and text. Error checking
// against these values is done at send-time, or upon running e.Check.
func NewEvent(title, text string) *Event {
return &Event{
Title: title,
Text: text,
}
}
// Check verifies that an event is valid.
func (e Event) Check() error {
if len(e.Title) == 0 {
return fmt.Errorf("statsd.Event title is required")
}
if len(e.Text) == 0 {
return fmt.Errorf("statsd.Event text is required")
}
return nil
}
// Encode returns the dogstatsd wire protocol representation for an event.
// Tags may be passed which will be added to the encoded output but not to
// the Event's list of tags, eg. for default tags.
func (e Event) Encode(tags ...string) (string, error) {
err := e.Check()
if err != nil {
return "", err
}
text := e.escapedText()
var buffer bytes.Buffer
buffer.WriteString("_e{")
buffer.WriteString(strconv.FormatInt(int64(len(e.Title)), 10))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatInt(int64(len(text)), 10))
buffer.WriteString("}:")
buffer.WriteString(e.Title)
buffer.WriteRune('|')
buffer.WriteString(text)
if !e.Timestamp.IsZero() {
buffer.WriteString("|d:")
buffer.WriteString(strconv.FormatInt(int64(e.Timestamp.Unix()), 10))
}
if len(e.Hostname) != 0 {
buffer.WriteString("|h:")
buffer.WriteString(e.Hostname)
}
if len(e.AggregationKey) != 0 {
buffer.WriteString("|k:")
buffer.WriteString(e.AggregationKey)
}
if len(e.Priority) != 0 {
buffer.WriteString("|p:")
buffer.WriteString(string(e.Priority))
}
if len(e.SourceTypeName) != 0 {
buffer.WriteString("|s:")
buffer.WriteString(e.SourceTypeName)
}
if len(e.AlertType) != 0 {
buffer.WriteString("|t:")
buffer.WriteString(string(e.AlertType))
}
writeTagString(&buffer, tags, e.Tags)
return buffer.String(), nil
}
// ServiceCheck support
type ServiceCheckStatus byte
const (
// Ok is the "ok" ServiceCheck status
Ok ServiceCheckStatus = 0
// Warn is the "warning" ServiceCheck status
Warn ServiceCheckStatus = 1
// Critical is the "critical" ServiceCheck status
Critical ServiceCheckStatus = 2
// Unknown is the "unknown" ServiceCheck status
Unknown ServiceCheckStatus = 3
)
// An ServiceCheck is an object that contains status of DataDog service check.
type ServiceCheck struct {
// Name of the service check. Required.
Name string
// Status of service check. Required.
Status ServiceCheckStatus
// Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd
// server will set this to the current time.
Timestamp time.Time
// Hostname for the serviceCheck.
Hostname string
// A message describing the current state of the serviceCheck.
Message string
// Tags for the serviceCheck.
Tags []string
}
// NewServiceCheck creates a new serviceCheck with the given name and status. Error checking
// against these values is done at send-time, or upon running sc.Check.
func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck {
return &ServiceCheck{
Name: name,
Status: status,
}
}
// Check verifies that an event is valid.
func (sc ServiceCheck) Check() error {
if len(sc.Name) == 0 {
return fmt.Errorf("statsd.ServiceCheck name is required")
}
if byte(sc.Status) < 0 || byte(sc.Status) > 3 {
return fmt.Errorf("statsd.ServiceCheck status has invalid value")
}
return nil
}
// Encode returns the dogstatsd wire protocol representation for an serviceCheck.
// Tags may be passed which will be added to the encoded output but not to
// the Event's list of tags, eg. for default tags.
func (sc ServiceCheck) Encode(tags ...string) (string, error) {
err := sc.Check()
if err != nil {
return "", err
}
message := sc.escapedMessage()
var buffer bytes.Buffer
buffer.WriteString("_sc|")
buffer.WriteString(sc.Name)
buffer.WriteRune('|')
buffer.WriteString(strconv.FormatInt(int64(sc.Status), 10))
if !sc.Timestamp.IsZero() {
buffer.WriteString("|d:")
buffer.WriteString(strconv.FormatInt(int64(sc.Timestamp.Unix()), 10))
}
if len(sc.Hostname) != 0 {
buffer.WriteString("|h:")
buffer.WriteString(sc.Hostname)
}
writeTagString(&buffer, tags, sc.Tags)
if len(message) != 0 {
buffer.WriteString("|m:")
buffer.WriteString(message)
}
return buffer.String(), nil
}
func (e Event) escapedText() string {
return strings.Replace(e.Text, "\n", "\\n", -1)
}
func (sc ServiceCheck) escapedMessage() string {
msg := strings.Replace(sc.Message, "\n", "\\n", -1)
return strings.Replace(msg, "m:", `m\:`, -1)
}
func removeNewlines(str string) string {
return strings.Replace(str, "\n", "", -1)
}
func writeTagString(w io.Writer, tagList1, tagList2 []string) {
// the tag lists may be shared with other callers, so we cannot modify
// them in any way (which means we cannot append to them either)
// therefore we must make an entirely separate copy just for this call
totalLen := len(tagList1) + len(tagList2)
if totalLen == 0 {
return
}
tags := make([]string, 0, totalLen)
tags = append(tags, tagList1...)
tags = append(tags, tagList2...)
io.WriteString(w, "|#")
io.WriteString(w, removeNewlines(tags[0]))
for _, tag := range tags[1:] {
io.WriteString(w, ",")
io.WriteString(w, removeNewlines(tag))
}
}

View File

@@ -0,0 +1,24 @@
Copyright (c) 2013 Julien Schmidt. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The names of the contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

28
vendor/github.com/buaazp/fasthttprouter/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2015-2016, 招牌疯子
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of uq nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

216
vendor/github.com/buaazp/fasthttprouter/README.md generated vendored Normal file
View File

@@ -0,0 +1,216 @@
# FastHttpRouter
[![Build Status](https://travis-ci.org/buaazp/fasthttprouter.svg?branch=master)](https://travis-ci.org/buaazp/fasthttprouter)
[![Coverage Status](https://coveralls.io/repos/buaazp/fasthttprouter/badge.svg?branch=master&service=github)](https://coveralls.io/github/buaazp/fasthttprouter?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/buaazp/fasthttprouter)](https://goreportcard.com/report/github.com/buaazp/fasthttprouter)
[![GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter?status.svg)](http://godoc.org/github.com/buaazp/fasthttprouter)
[![GitHub release](https://img.shields.io/github/release/buaazp/fasthttprouter.svg)](https://github.com/buaazp/fasthttprouter/releases)
FastHttpRouter is forked from [httprouter](https://github.com/julienschmidt/httprouter) which is a lightweight high performance HTTP request router
(also called *multiplexer* or just *mux* for short) for [fasthttp](https://github.com/valyala/fasthttp).
This router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.
#### License Related
- The author of `httprouter` [@julienschmidt](https://github.com/julienschmidt) did almost all the hard work of this router.
- I respect the laws of open source. So LICENSE of `httprouter` is alway stay here: [HttpRouterLicense](HttpRouterLicense).
- What I do is just fit for `fasthttp`. I have no hope to build a huge but toxic go web framwork like [iris](https://github.com/kataras/iris).
- I fork this repo is just because there is no router for `fasthttp` at that time. And `fasthttprouter` is the FIRST router for `fasthttp`.
- `fasthttprouter` has been used in my online production and processes 17 million requests per day. It is fast and stable, so I decide to release a stable version.
#### Releases
- [2016.10.24] [v0.1.0](https://github.com/buaazp/fasthttprouter/releases/tag/v0.1.0) The first release version of `fasthttprouter`.
## Features
**Best Performance:** FastHttpRouter is **one of the fastest** go web frameworks in the [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark). Even faster than httprouter itself.
- Basic Test: The first test case is to mock 0 ms, 10 ms, 100 ms, 500 ms processing time in handlers. The concurrency clients are 5000.
![](http://ww3.sinaimg.cn/large/4c422e03jw1f2p6nyqh9ij20mm0aktbj.jpg)
- Concurrency Test: In 30 ms processing time, the tets result for 100, 1000, 5000 clients is:
![](http://ww4.sinaimg.cn/large/4c422e03jw1f2p6o1cdbij20lk09sack.jpg)
See below for technical details of the implementation.
**Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux),
a requested URL path could match multiple patterns. Therefore they have some
awkward pattern priority rules, like *longest match* or *first registered,
first matched*. By design of this router, a request can only match exactly one
or no route. As a result, there are also no unintended matches, which makes it
great for SEO and improves the user experience.
**Stop caring about trailing slashes:** Choose the URL style you like, the
router automatically redirects the client if a trailing slash is missing or if
there is one extra. Of course it only does so, if the new path has a handler.
If you don't like it, you can [turn off this behavior](http://godoc.org/github.com/buaazp/fasthttprouter#Router.RedirectTrailingSlash).
**Path auto-correction:** Besides detecting the missing or additional trailing
slash at no extra cost, the router can also fix wrong cases and remove
superfluous path elements (like `../` or `//`).
Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users?
FastHttpRouter can help him by making a case-insensitive look-up and redirecting him
to the correct URL.
**Parameters in your routing pattern:** Stop parsing the requested URL path,
just give the path segment a name and the router delivers the dynamic value to
you. Because of the design of the router, path parameters are very cheap.
**Zero Garbage:** The matching and dispatching process generates zero bytes of
garbage. In fact, the only heap allocations that are made, is by building the
slice of the key-value pairs for path parameters. If the request path contains
no parameters, not a single heap allocation is necessary.
**No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/buaazp/fasthttprouter#Router.PanicHandler) to deal with panics
occurring during handling a HTTP request. The router then recovers and lets the
PanicHandler log what happened and deliver a nice error page.
**Perfect for APIs:** The router design encourages to build sensible, hierarchical
RESTful APIs. Moreover it has builtin native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html)
and `405 Method Not Allowed` replies.
Of course you can also set **custom [NotFound](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) and [MethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](http://godoc.org/github.com/buaazp/fasthttprouter#Router.ServeFiles).
## Usage
This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter) for details:
Let's start with a trivial example:
```go
package main
import (
"fmt"
"log"
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp"
)
func Index(ctx *fasthttp.RequestCtx) {
fmt.Fprint(ctx, "Welcome!\n")
}
func Hello(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name"))
}
func main() {
router := fasthttprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler))
}
```
### Named parameters
As you can see, `:name` is a *named parameter*. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`.
Named parameters only match a single path segment:
```
Pattern: /user/:user
/user/gordon match
/user/you match
/user/gordon/profile no match
/user/ no match
```
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other.
### Catch-All parameters
The second type are *catch-all* parameters and have the form `*name`.
Like the name suggests, they match everything.
Therefore they must always be at the **end** of the pattern:
```
Pattern: /src/*filepath
/src/ match
/src/somefile.go match
/src/subdir/somefile.go match
```
## How does it work?
The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like:
```
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
```
Every `*<num>` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show][benchmark], this works very well and efficient.
Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree.
For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways:
1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.
```
├------------
├---------
├-----
├----
├--
├--
└-
```
## Why doesn't this work with `http.Handler`?
Becasue fasthttp doesn't provide http.Handler. See this [description](https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp).
Fasthttp works with [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler) functions instead of objects implementing Handler interface. So a FastHttpRouter provides a [Handler](https://godoc.org/github.com/buaazp/fasthttprouter#Router.Handler) interface to implement the fasthttp.ListenAndServe interface.
Just try it out for yourself, the usage of FastHttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up.
## Where can I find Middleware *X*?
This package just provides a very efficient request router with a few extra features. The router is just a [`fasthttp.RequestHandler`](https://godoc.org/github.com/valyala/fasthttp#RequestHandler), you can chain any `fasthttp.RequestHandler` compatible middleware before the router. Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy!
Have a look at these midware examples:
- [Auth Midware](examples/auth)
- [Multi Hosts Midware](examples/hosts)
## Chaining with the NotFound handler
**NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.**
You can use another [http.Handler](http://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) handler. This allows chaining.
### Static files
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets):
```go
// Serve static files from the ./public directory
router.NotFound = fasthttp.FSHandler("./public", 0)
```
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
## Web Frameworks based on FastHttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
- Waiting for you to do this...

123
vendor/github.com/buaazp/fasthttprouter/path.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package fasthttprouter
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
//
// The following rules are applied iteratively until no further processing can
// be done:
// 1. Replace multiple slashes with a single slash.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path.
//
// If the result of this process is an empty string, "/" is returned
func CleanPath(p string) string {
// Turn empty string into "/"
if p == "" {
return "/"
}
n := len(p)
var buf []byte
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// path must start with '/'
r := 1
w := 1
if p[0] != '/' {
r = 0
buf = make([]byte, n+1)
buf[0] = '/'
}
trailing := n > 2 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this
// loop has no expensive function calls (except 1x make)
for r < n {
switch {
case p[r] == '/':
// empty path element, trailing slash is added after the end
r++
case p[r] == '.' && r+1 == n:
trailing = true
r++
case p[r] == '.' && p[r+1] == '/':
// . element
r++
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
r += 2
if w > 1 {
// can backtrack
w--
if buf == nil {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
default:
// real path element.
// add slash if needed
if w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// copy element
for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r])
w++
r++
}
}
}
// re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
if buf == nil {
return p[:w]
}
return string(buf[:w])
}
// internal helper to lazily create a buffer if necessary
func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil {
if s[w] == c {
return
}
*buf = make([]byte, len(s))
copy(*buf, s[:w])
}
(*buf)[w] = c
}

374
vendor/github.com/buaazp/fasthttprouter/router.go generated vendored Normal file
View File

@@ -0,0 +1,374 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// Package fasthttprouter is a trie based high performance HTTP request router.
//
// A trivial example is:
//
// package main
// import (
// "fmt"
// "log"
//
// "github.com/buaazp/fasthttprouter"
// "github.com/valyala/fasthttp"
// )
// func Index(ctx *fasthttp.RequestCtx) {
// fmt.Fprint(ctx, "Welcome!\n")
// }
// func Hello(ctx *fasthttp.RequestCtx) {
// fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name"))
// }
// func main() {
// router := fasthttprouter.New()
// router.GET("/", Index)
// router.GET("/hello/:name", Hello)
// log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler))
// }
//
// The router matches incoming requests by the request method and the path.
// If a handle is registered for this path and method, the router delegates the
// request to that function.
// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to
// register handles, for all other methods router.Handle can be used.
//
// The registered path, against which the router matches incoming requests, can
// contain two types of parameters:
// Syntax Type
// :name named parameter
// *name catch-all parameter
//
// Named parameters are dynamic path segments. They match anything until the
// next '/' or the path end:
// Path: /blog/:category/:post
//
// Requests:
// /blog/go/request-routers match: category="go", post="request-routers"
// /blog/go/request-routers/ no match, but the router would redirect
// /blog/go/ no match
// /blog/go/request-routers/comments no match
//
// Catch-all parameters match anything until the path end, including the
// directory index (the '/' before the catch-all). Since they match anything
// until the end, catch-all parameters must always be the final path element.
// Path: /files/*filepath
//
// Requests:
// /files/ match: filepath="/"
// /files/LICENSE match: filepath="/LICENSE"
// /files/templates/article.html match: filepath="/templates/article.html"
// /files no match, but the router would redirect
//
// The value of parameters is inside ctx.UserValue
// To retrieve the value of a parameter:
// // use the name of the parameter
// user := ps.UserValue("user")
//
package fasthttprouter
import (
"strings"
"github.com/valyala/fasthttp"
)
var (
defaultContentType = []byte("text/plain; charset=utf-8")
questionMark = []byte("?")
)
// Router is a http.Handler which can be used to dispatch requests to different
// handler functions via configurable routes
type Router struct {
trees map[string]*node
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
// If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
// If enabled, the router automatically replies to OPTIONS requests.
// Custom OPTIONS handlers take priority over automatic replies.
HandleOPTIONS bool
// Configurable http.Handler which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound fasthttp.RequestHandler
// Configurable http.Handler which is called when a request
// cannot be routed and HandleMethodNotAllowed is true.
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
// The "Allow" header with allowed request methods is set before the handler
// is called.
MethodNotAllowed fasthttp.RequestHandler
// Function to handle panics recovered from http handlers.
// It should be used to generate a error page and return the http error code
// 500 (Internal Server Error).
// The handler can be used to keep your server from crashing because of
// unrecovered panics.
PanicHandler func(*fasthttp.RequestCtx, interface{})
}
// New returns a new initialized Router.
// Path auto-correction, including trailing slashes, is enabled by default.
func New() *Router {
return &Router{
RedirectTrailingSlash: true,
RedirectFixedPath: true,
HandleMethodNotAllowed: true,
HandleOPTIONS: true,
}
}
// GET is a shortcut for router.Handle("GET", path, handle)
func (r *Router) GET(path string, handle fasthttp.RequestHandler) {
r.Handle("GET", path, handle)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func (r *Router) HEAD(path string, handle fasthttp.RequestHandler) {
r.Handle("HEAD", path, handle)
}
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
func (r *Router) OPTIONS(path string, handle fasthttp.RequestHandler) {
r.Handle("OPTIONS", path, handle)
}
// POST is a shortcut for router.Handle("POST", path, handle)
func (r *Router) POST(path string, handle fasthttp.RequestHandler) {
r.Handle("POST", path, handle)
}
// PUT is a shortcut for router.Handle("PUT", path, handle)
func (r *Router) PUT(path string, handle fasthttp.RequestHandler) {
r.Handle("PUT", path, handle)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
func (r *Router) PATCH(path string, handle fasthttp.RequestHandler) {
r.Handle("PATCH", path, handle)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func (r *Router) DELETE(path string, handle fasthttp.RequestHandler) {
r.Handle("DELETE", path, handle)
}
// Handle registers a new request handle with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (r *Router) Handle(method, path string, handle fasthttp.RequestHandler) {
if path[0] != '/' {
panic("path must begin with '/' in path '" + path + "'")
}
if r.trees == nil {
r.trees = make(map[string]*node)
}
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
}
root.addRoute(path, handle)
}
// ServeFiles serves files from the given file system root.
// The path must end with "/*filepath", files are then served from the local
// path /defined/root/dir/*filepath.
// For example if root is "/etc" and *filepath is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// router.ServeFiles("/src/*filepath", "/var/www")
func (r *Router) ServeFiles(path string, rootPath string) {
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
panic("path must end with /*filepath in path '" + path + "'")
}
prefix := path[:len(path)-10]
fileHandler := fasthttp.FSHandler(rootPath, strings.Count(prefix, "/"))
r.GET(path, func(ctx *fasthttp.RequestCtx) {
fileHandler(ctx)
})
}
func (r *Router) recv(ctx *fasthttp.RequestCtx) {
if rcv := recover(); rcv != nil {
r.PanicHandler(ctx, rcv)
}
}
// Lookup allows the manual lookup of a method + path combo.
// This is e.g. useful to build a framework around this router.
// If the path was found, it returns the handle function and the path parameter
// values. Otherwise the third return value indicates whether a redirection to
// the same path with an extra / without the trailing slash should be performed.
func (r *Router) Lookup(method, path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) {
if root := r.trees[method]; root != nil {
return root.getValue(path, ctx)
}
return nil, false
}
func (r *Router) allowed(path, reqMethod string) (allow string) {
if path == "*" || path == "/*" { // server-wide
for method := range r.trees {
if method == "OPTIONS" {
continue
}
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
} else { // specific path
for method := range r.trees {
// Skip the requested method - we already tried this one
if method == reqMethod || method == "OPTIONS" {
continue
}
handle, _ := r.trees[method].getValue(path, nil)
if handle != nil {
// add request method to list of allowed methods
if len(allow) == 0 {
allow = method
} else {
allow += ", " + method
}
}
}
}
if len(allow) > 0 {
allow += ", OPTIONS"
}
return
}
// Handler makes the router implement the fasthttp.ListenAndServe interface.
func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
if r.PanicHandler != nil {
defer r.recv(ctx)
}
path := string(ctx.Path())
method := string(ctx.Method())
if root := r.trees[method]; root != nil {
if f, tsr := root.getValue(path, ctx); f != nil {
f(ctx)
return
} else if method != "CONNECT" && path != "/" {
code := 301 // Permanent redirect, request with GET method
if method != "GET" {
// Temporary redirect, request with same method
// As of Go 1.3, Go does not support status code 308.
code = 307
}
if tsr && r.RedirectTrailingSlash {
var uri string
if len(path) > 1 && path[len(path)-1] == '/' {
uri = path[:len(path)-1]
} else {
uri = path + "/"
}
ctx.Redirect(uri, code)
return
}
// Try to fix the request path
if r.RedirectFixedPath {
fixedPath, found := root.findCaseInsensitivePath(
CleanPath(path),
r.RedirectTrailingSlash,
)
if found {
queryBuf := ctx.URI().QueryString()
if len(queryBuf) > 0 {
fixedPath = append(fixedPath, questionMark...)
fixedPath = append(fixedPath, queryBuf...)
}
uri := string(fixedPath)
ctx.Redirect(uri, code)
return
}
}
}
}
if method == "OPTIONS" {
// Handle OPTIONS requests
if r.HandleOPTIONS {
if allow := r.allowed(path, method); len(allow) > 0 {
ctx.Response.Header.Set("Allow", allow)
return
}
}
} else {
// Handle 405
if r.HandleMethodNotAllowed {
if allow := r.allowed(path, method); len(allow) > 0 {
ctx.Response.Header.Set("Allow", allow)
if r.MethodNotAllowed != nil {
r.MethodNotAllowed(ctx)
} else {
ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed)
ctx.SetContentTypeBytes(defaultContentType)
ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed))
}
return
}
}
}
// Handle 404
if r.NotFound != nil {
r.NotFound(ctx)
} else {
ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound),
fasthttp.StatusNotFound)
}
}

643
vendor/github.com/buaazp/fasthttprouter/tree.go generated vendored Normal file
View File

@@ -0,0 +1,643 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package fasthttprouter
import (
"github.com/valyala/fasthttp"
"strings"
"unicode"
"unicode/utf8"
)
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func countParams(path string) uint8 {
var n uint
for i := 0; i < len(path); i++ {
if path[i] != ':' && path[i] != '*' {
continue
}
n++
}
if n >= 255 {
return 255
}
return uint8(n)
}
type nodeType uint8
const (
static nodeType = iota // default
root
param
catchAll
)
type node struct {
path string
wildChild bool
nType nodeType
maxParams uint8
indices string
children []*node
handle fasthttp.RequestHandler
priority uint32
}
// increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int {
n.children[pos].priority++
prio := n.children[pos].priority
// adjust position (move to front)
newPos := pos
for newPos > 0 && n.children[newPos-1].priority < prio {
// swap node positions
tmpN := n.children[newPos-1]
n.children[newPos-1] = n.children[newPos]
n.children[newPos] = tmpN
newPos--
}
// build new index char string
if newPos != pos {
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
n.indices[pos:pos+1] + // the index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
}
return newPos
}
// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handle fasthttp.RequestHandler) {
fullPath := path
n.priority++
numParams := countParams(path)
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// Update maxParams of the current node
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
i := 0
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {
i++
}
// Split edge
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handle: n.handle,
priority: n.priority - 1,
}
// Update maxParams (max of all children)
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
// Make new node a child of this node
if i < len(path) {
path = path[i:]
if n.wildChild {
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
} else {
// Wildcard conflict
pathSeg := strings.SplitN(path, "/", 2)[0]
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
}
c := path[0]
// slash after param
if n.nType == param && c == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handle)
return
} else if i == len(path) { // Make node a (in-path) leaf
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
}
return
}
} else { // Empty tree
n.insertChild(numParams, path, fullPath, handle)
n.nType = root
}
}
func (n *node) insertChild(numParams uint8, path, fullPath string, handle fasthttp.RequestHandler) {
var offset int // already handled bytes of the path
// find prefix until first wildcard (beginning with ':'' or '*'')
for i, max := 0, len(path); numParams > 0; i++ {
c := path[i]
if c != ':' && c != '*' {
continue
}
// find wildcard end (either '/' or path end)
end := i + 1
for end < max && path[end] != '/' {
switch path[end] {
// the wildcard name must not contain ':' and '*'
case ':', '*':
panic("only one wildcard per path segment is allowed, has: '" +
path[i:] + "' in path '" + fullPath + "'")
default:
end++
}
}
// check if this Node existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
panic("wildcard route '" + path[i:end] +
"' conflicts with existing children in path '" + fullPath + "'")
}
// check if the wildcard has a name
if end-i < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
if c == ':' { // param
// split path at the beginning of the wildcard
if i > 0 {
n.path = path[offset:i]
offset = i
}
child := &node{
nType: param,
maxParams: numParams,
}
n.children = []*node{child}
n.wildChild = true
n = child
n.priority++
numParams--
// if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/'
if end < max {
n.path = path[offset:end]
offset = end
child := &node{
maxParams: numParams,
priority: 1,
}
n.children = []*node{child}
n = child
}
} else { // catchAll
if end != max || numParams > 1 {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
}
// currently fixed width 1 for '/'
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
n.path = path[offset:i]
// first node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
}
n.children = []*node{child}
n.indices = string(path[i])
n = child
n.priority++
// second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return
}
}
// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handle = handle
}
// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string, ctx *fasthttp.RequestCtx) (handle fasthttp.RequestHandler, tsr bool) {
walk: // outer loop for walking the tree
for {
if len(path) > len(n.path) {
if path[:len(n.path)] == n.path {
path = path[len(n.path):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
if !n.wildChild {
c := path[0]
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
n = n.children[i]
continue walk
}
}
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handle != nil)
return
}
// handle wildcard child
n = n.children[0]
switch n.nType {
case param:
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// handle calls to Router.allowed method with nil context
if ctx != nil {
ctx.SetUserValue(n.path[1:], path[:end])
}
// we need to go deeper!
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
}
// ... but we can't
tsr = (len(path) == end+1)
return
}
if handle = n.handle; handle != nil {
return
} else if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
tsr = (n.path == "/" && n.handle != nil)
}
return
case catchAll:
if ctx != nil {
// save param value
ctx.SetUserValue(n.path[2:], path)
}
handle = n.handle
return
default:
panic("invalid node type")
}
}
} else if path == n.path {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if handle = n.handle; handle != nil {
return
}
if path == "/" && n.wildChild && n.nType != root {
tsr = true
return
}
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
tsr = (len(n.path) == 1 && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil)
return
}
}
return
}
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handle != nil)
return
}
}
// Makes a case-insensitive lookup of the given path and tries to find a handler.
// It can optionally also fix trailing slashes.
// It returns the case-corrected path and a bool indicating whether the lookup
// was successful.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
return n.findCaseInsensitivePathRec(
path,
strings.ToLower(path),
make([]byte, 0, len(path)+1), // preallocate enough memory for new path
[4]byte{}, // empty rune buffer
fixTrailingSlash,
)
}
// shift bytes in array by n bytes left
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
switch n {
case 0:
return rb
case 1:
return [4]byte{rb[1], rb[2], rb[3], 0}
case 2:
return [4]byte{rb[2], rb[3]}
case 3:
return [4]byte{rb[3]}
default:
return [4]byte{}
}
}
// recursive case-insensitive lookup function used by n.findCaseInsensitivePath
func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) {
loNPath := strings.ToLower(n.path)
walk: // outer loop for walking the tree
for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) {
// add common path to result
ciPath = append(ciPath, n.path...)
if path = path[len(n.path):]; len(path) > 0 {
loOld := loPath
loPath = loPath[len(loNPath):]
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
if !n.wildChild {
// skip rune bytes already processed
rb = shiftNRuneBytes(rb, len(loNPath))
if rb[0] != 0 {
// old rune not finished
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == rb[0] {
// continue with child node
n = n.children[i]
loNPath = strings.ToLower(n.path)
continue walk
}
}
} else {
// process a new rune
var rv rune
// find rune start
// runes are up to 4 byte long,
// -4 would definitely be another rune
var off int
for max := min(len(loNPath), 3); off < max; off++ {
if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) {
// read rune from cached lowercase path
rv, _ = utf8.DecodeRuneInString(loOld[i:])
break
}
}
// calculate lowercase bytes of current rune
utf8.EncodeRune(rb[:], rv)
// skipp already processed bytes
rb = shiftNRuneBytes(rb, off)
for i := 0; i < len(n.indices); i++ {
// lowercase matches
if n.indices[i] == rb[0] {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out, found := n.children[i].findCaseInsensitivePathRec(
path, loPath, ciPath, rb, fixTrailingSlash,
); found {
return out, true
}
break
}
}
// same for uppercase rune, if it differs
if up := unicode.ToUpper(rv); up != rv {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
for i := 0; i < len(n.indices); i++ {
// uppercase matches
if n.indices[i] == rb[0] {
// continue with child node
n = n.children[i]
loNPath = strings.ToLower(n.path)
continue walk
}
}
}
}
// Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path
return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil)
}
n = n.children[0]
switch n.nType {
case param:
// find param end (either '/' or path end)
k := 0
for k < len(path) && path[k] != '/' {
k++
}
// add param value to case insensitive path
ciPath = append(ciPath, path[:k]...)
// we need to go deeper!
if k < len(path) {
if len(n.children) > 0 {
// continue with child node
n = n.children[0]
loNPath = strings.ToLower(n.path)
loPath = loPath[k:]
path = path[k:]
continue
}
// ... but we can't
if fixTrailingSlash && len(path) == k+1 {
return ciPath, true
}
return ciPath, false
}
if n.handle != nil {
return ciPath, true
} else if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists
n = n.children[0]
if n.path == "/" && n.handle != nil {
return append(ciPath, '/'), true
}
}
return ciPath, false
case catchAll:
return append(ciPath, path...), true
default:
panic("invalid node type")
}
} else {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handle != nil {
return ciPath, true
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
if (len(n.path) == 1 && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil) {
return append(ciPath, '/'), true
}
return ciPath, false
}
}
}
return ciPath, false
}
}
// Nothing found.
// Try to fix the path by adding / removing a trailing slash
if fixTrailingSlash {
if path == "/" {
return ciPath, true
}
if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' &&
loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil {
return append(ciPath, n.path...), true
}
}
return ciPath, false
}

3
vendor/github.com/certifi/gocertifi/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,3 @@
This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
one at http://mozilla.org/MPL/2.0/.

60
vendor/github.com/certifi/gocertifi/README.md generated vendored Normal file
View File

@@ -0,0 +1,60 @@
# GoCertifi: SSL Certificates for Golang
This Go package contains a CA bundle that you can reference in your Go code.
This is useful for systems that do not have CA bundles that Golang can find
itself, or where a uniform set of CAs is valuable.
This is the same CA bundle that ships with the
[Python Requests](https://github.com/kennethreitz/requests) library, and is a
Golang specific port of [certifi](https://github.com/kennethreitz/certifi). The
CA bundle is derived from Mozilla's canonical set.
## Usage
You can use the `gocertifi` package as follows:
```go
import "github.com/certifi/gocertifi"
cert_pool, err := gocertifi.CACerts()
```
You can use the returned `*x509.CertPool` as part of an HTTP transport, for example:
```go
import (
"net/http"
"crypto/tls"
)
// Setup an HTTP client with a custom transport
transport := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: cert_pool},
}
client := &http.Client{Transport: transport}
// Make an HTTP request using our custom transport
resp, err := client.Get("https://example.com")
```
## Detailed Documentation
Import as follows:
```go
import "github.com/certifi/gocertifi"
```
### Errors
```go
var ErrParseFailed = errors.New("gocertifi: error when parsing certificates")
```
### Functions
```go
func CACerts() (*x509.CertPool, error)
```
CACerts builds an X.509 certificate pool containing the Mozilla CA Certificate
bundle. Returns nil on error along with an appropriate error code.

5298
vendor/github.com/certifi/gocertifi/certifi.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

20
vendor/github.com/certifi/gocertifi/tasks.py generated vendored Normal file
View File

@@ -0,0 +1,20 @@
from invoke import task
import requests
@task
def update(ctx):
r = requests.get('https://mkcert.org/generate/')
r.raise_for_status()
certs = r.content
with open('certifi.go', 'rb') as f:
file = f.read()
file = file.split('`\n')
assert len(file) == 3
file[1] = certs
ctx.run("rm certifi.go")
with open('certifi.go', 'wb') as f:
f.write('`\n'.join(file))

13
vendor/github.com/getsentry/raven-go/Dockerfile.test generated vendored Normal file
View File

@@ -0,0 +1,13 @@
FROM golang:1.7
RUN mkdir -p /go/src/github.com/getsentry/raven-go
WORKDIR /go/src/github.com/getsentry/raven-go
ENV GOPATH /go
RUN go install -race std && go get golang.org/x/tools/cmd/cover
COPY . /go/src/github.com/getsentry/raven-go
RUN go get -v ./...
CMD ["./runtests.sh"]

28
vendor/github.com/getsentry/raven-go/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
Copyright (c) 2015 Functional Software, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Apollic Software, LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

13
vendor/github.com/getsentry/raven-go/README.md generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# raven [![Build Status](https://travis-ci.org/getsentry/raven-go.png?branch=master)](https://travis-ci.org/getsentry/raven-go)
raven is a Go client for the [Sentry](https://github.com/getsentry/sentry)
event/error logging system.
- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go)
- [**Usage and Examples**](https://docs.sentry.io/clients/go/)
## Installation
```text
go get github.com/getsentry/raven-go
```

799
vendor/github.com/getsentry/raven-go/client.go generated vendored Normal file
View File

@@ -0,0 +1,799 @@
// Package raven implements a client for the Sentry error logging service.
package raven
import (
"bytes"
"compress/zlib"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/certifi/gocertifi"
)
const (
userAgent = "raven-go/1.0"
timestampFormat = `"2006-01-02T15:04:05.00"`
)
var (
ErrPacketDropped = errors.New("raven: packet dropped")
ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON")
ErrMissingUser = errors.New("raven: dsn missing public key and/or password")
ErrMissingPrivateKey = errors.New("raven: dsn missing private key")
ErrMissingProjectID = errors.New("raven: dsn missing project id")
)
type Severity string
// http://docs.python.org/2/howto/logging.html#logging-levels
const (
DEBUG = Severity("debug")
INFO = Severity("info")
WARNING = Severity("warning")
ERROR = Severity("error")
FATAL = Severity("fatal")
)
type Timestamp time.Time
func (t Timestamp) MarshalJSON() ([]byte, error) {
return []byte(time.Time(t).UTC().Format(timestampFormat)), nil
}
func (timestamp *Timestamp) UnmarshalJSON(data []byte) error {
t, err := time.Parse(timestampFormat, string(data))
if err != nil {
return err
}
*timestamp = Timestamp(t)
return nil
}
// An Interface is a Sentry interface that will be serialized as JSON.
// It must implement json.Marshaler or use json struct tags.
type Interface interface {
// The Sentry class name. Example: sentry.interfaces.Stacktrace
Class() string
}
type Culpriter interface {
Culprit() string
}
type Transport interface {
Send(url, authHeader string, packet *Packet) error
}
type outgoingPacket struct {
packet *Packet
ch chan error
}
type Tag struct {
Key string
Value string
}
type Tags []Tag
func (tag *Tag) MarshalJSON() ([]byte, error) {
return json.Marshal([2]string{tag.Key, tag.Value})
}
func (t *Tag) UnmarshalJSON(data []byte) error {
var tag [2]string
if err := json.Unmarshal(data, &tag); err != nil {
return err
}
*t = Tag{tag[0], tag[1]}
return nil
}
func (t *Tags) UnmarshalJSON(data []byte) error {
var tags []Tag
switch data[0] {
case '[':
// Unmarshal into []Tag
if err := json.Unmarshal(data, &tags); err != nil {
return err
}
case '{':
// Unmarshal into map[string]string
tagMap := make(map[string]string)
if err := json.Unmarshal(data, &tagMap); err != nil {
return err
}
// Convert to []Tag
for k, v := range tagMap {
tags = append(tags, Tag{k, v})
}
default:
return ErrUnableToUnmarshalJSON
}
*t = tags
return nil
}
// https://docs.getsentry.com/hosted/clientdev/#building-the-json-packet
type Packet struct {
// Required
Message string `json:"message"`
// Required, set automatically by Client.Send/Report via Packet.Init if blank
EventID string `json:"event_id"`
Project string `json:"project"`
Timestamp Timestamp `json:"timestamp"`
Level Severity `json:"level"`
Logger string `json:"logger"`
// Optional
Platform string `json:"platform,omitempty"`
Culprit string `json:"culprit,omitempty"`
ServerName string `json:"server_name,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
Tags Tags `json:"tags,omitempty"`
Modules map[string]string `json:"modules,omitempty"`
Fingerprint []string `json:"fingerprint,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
Interfaces []Interface `json:"-"`
}
// NewPacket constructs a packet with the specified message and interfaces.
func NewPacket(message string, interfaces ...Interface) *Packet {
extra := map[string]interface{}{
"runtime.Version": runtime.Version(),
"runtime.NumCPU": runtime.NumCPU(),
"runtime.GOMAXPROCS": runtime.GOMAXPROCS(0), // 0 just returns the current value
"runtime.NumGoroutine": runtime.NumGoroutine(),
}
return &Packet{
Message: message,
Interfaces: interfaces,
Extra: extra,
}
}
// Init initializes required fields in a packet. It is typically called by
// Client.Send/Report automatically.
func (packet *Packet) Init(project string) error {
if packet.Project == "" {
packet.Project = project
}
if packet.EventID == "" {
var err error
packet.EventID, err = uuid()
if err != nil {
return err
}
}
if time.Time(packet.Timestamp).IsZero() {
packet.Timestamp = Timestamp(time.Now())
}
if packet.Level == "" {
packet.Level = ERROR
}
if packet.Logger == "" {
packet.Logger = "root"
}
if packet.ServerName == "" {
packet.ServerName = hostname
}
if packet.Platform == "" {
packet.Platform = "go"
}
if packet.Culprit == "" {
for _, inter := range packet.Interfaces {
if c, ok := inter.(Culpriter); ok {
packet.Culprit = c.Culprit()
if packet.Culprit != "" {
break
}
}
}
}
return nil
}
func (packet *Packet) AddTags(tags map[string]string) {
for k, v := range tags {
packet.Tags = append(packet.Tags, Tag{k, v})
}
}
func uuid() (string, error) {
id := make([]byte, 16)
_, err := io.ReadFull(rand.Reader, id)
if err != nil {
return "", err
}
id[6] &= 0x0F // clear version
id[6] |= 0x40 // set version to 4 (random uuid)
id[8] &= 0x3F // clear variant
id[8] |= 0x80 // set to IETF variant
return hex.EncodeToString(id), nil
}
func (packet *Packet) JSON() ([]byte, error) {
packetJSON, err := json.Marshal(packet)
if err != nil {
return nil, err
}
interfaces := make(map[string]Interface, len(packet.Interfaces))
for _, inter := range packet.Interfaces {
if inter != nil {
interfaces[inter.Class()] = inter
}
}
if len(interfaces) > 0 {
interfaceJSON, err := json.Marshal(interfaces)
if err != nil {
return nil, err
}
packetJSON[len(packetJSON)-1] = ','
packetJSON = append(packetJSON, interfaceJSON[1:]...)
}
return packetJSON, nil
}
type context struct {
user *User
http *Http
tags map[string]string
}
func (c *context) SetUser(u *User) { c.user = u }
func (c *context) SetHttp(h *Http) { c.http = h }
func (c *context) SetTags(t map[string]string) {
if c.tags == nil {
c.tags = make(map[string]string)
}
for k, v := range t {
c.tags[k] = v
}
}
func (c *context) Clear() {
c.user = nil
c.http = nil
c.tags = nil
}
// Return a list of interfaces to be used in appending with the rest
func (c *context) interfaces() []Interface {
len, i := 0, 0
if c.user != nil {
len++
}
if c.http != nil {
len++
}
interfaces := make([]Interface, len)
if c.user != nil {
interfaces[i] = c.user
i++
}
if c.http != nil {
interfaces[i] = c.http
i++
}
return interfaces
}
// The maximum number of packets that will be buffered waiting to be delivered.
// Packets will be dropped if the buffer is full. Used by NewClient.
var MaxQueueBuffer = 100
func newTransport() Transport {
t := &HTTPTransport{}
rootCAs, err := gocertifi.CACerts()
if err != nil {
log.Println("raven: failed to load root TLS certificates:", err)
} else {
t.Client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
},
}
}
return t
}
func newClient(tags map[string]string) *Client {
client := &Client{
Transport: newTransport(),
Tags: tags,
context: &context{},
queue: make(chan *outgoingPacket, MaxQueueBuffer),
}
client.SetDSN(os.Getenv("SENTRY_DSN"))
return client
}
// New constructs a new Sentry client instance
func New(dsn string) (*Client, error) {
client := newClient(nil)
return client, client.SetDSN(dsn)
}
// NewWithTags constructs a new Sentry client instance with default tags.
func NewWithTags(dsn string, tags map[string]string) (*Client, error) {
client := newClient(tags)
return client, client.SetDSN(dsn)
}
// NewClient constructs a Sentry client and spawns a background goroutine to
// handle packets sent by Client.Report.
//
// Deprecated: use New and NewWithTags instead
func NewClient(dsn string, tags map[string]string) (*Client, error) {
client := newClient(tags)
return client, client.SetDSN(dsn)
}
// Client encapsulates a connection to a Sentry server. It must be initialized
// by calling NewClient. Modification of fields concurrently with Send or after
// calling Report for the first time is not thread-safe.
type Client struct {
Tags map[string]string
Transport Transport
// DropHandler is called when a packet is dropped because the buffer is full.
DropHandler func(*Packet)
// Context that will get appending to all packets
context *context
mu sync.RWMutex
url string
projectID string
authHeader string
release string
environment string
includePaths []string
queue chan *outgoingPacket
// A WaitGroup to keep track of all currently in-progress captures
// This is intended to be used with Client.Wait() to assure that
// all messages have been transported before exiting the process.
wg sync.WaitGroup
// A Once to track only starting up the background worker once
start sync.Once
}
// Initialize a default *Client instance
var DefaultClient = newClient(nil)
// SetDSN updates a client with a new DSN. It safe to call after and
// concurrently with calls to Report and Send.
func (client *Client) SetDSN(dsn string) error {
if dsn == "" {
return nil
}
client.mu.Lock()
defer client.mu.Unlock()
uri, err := url.Parse(dsn)
if err != nil {
return err
}
if uri.User == nil {
return ErrMissingUser
}
publicKey := uri.User.Username()
secretKey, ok := uri.User.Password()
if !ok {
return ErrMissingPrivateKey
}
uri.User = nil
if idx := strings.LastIndex(uri.Path, "/"); idx != -1 {
client.projectID = uri.Path[idx+1:]
uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/"
}
if client.projectID == "" {
return ErrMissingProjectID
}
client.url = uri.String()
client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey)
return nil
}
// Sets the DSN for the default *Client instance
func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) }
// SetRelease sets the "release" tag.
func (client *Client) SetRelease(release string) {
client.mu.Lock()
defer client.mu.Unlock()
client.release = release
}
// SetEnvironment sets the "environment" tag.
func (client *Client) SetEnvironment(environment string) {
client.mu.Lock()
defer client.mu.Unlock()
client.environment = environment
}
// SetRelease sets the "release" tag on the default *Client
func SetRelease(release string) { DefaultClient.SetRelease(release) }
// SetEnvironment sets the "environment" tag on the default *Client
func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) }
func (client *Client) worker() {
for outgoingPacket := range client.queue {
client.mu.RLock()
url, authHeader := client.url, client.authHeader
client.mu.RUnlock()
outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet)
client.wg.Done()
}
}
// Capture asynchronously delivers a packet to the Sentry server. It is a no-op
// when client is nil. A channel is provided if it is important to check for a
// send's success.
func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
if client == nil {
return
}
// Keep track of all running Captures so that we can wait for them all to finish
// *Must* call client.wg.Done() on any path that indicates that an event was
// finished being acted upon, whether success or failure
client.wg.Add(1)
ch = make(chan error, 1)
// Merge capture tags and client tags
packet.AddTags(captureTags)
packet.AddTags(client.Tags)
packet.AddTags(client.context.tags)
// Initialize any required packet fields
client.mu.RLock()
projectID := client.projectID
release := client.release
environment := client.environment
client.mu.RUnlock()
err := packet.Init(projectID)
if err != nil {
ch <- err
client.wg.Done()
return
}
packet.Release = release
packet.Environment = environment
outgoingPacket := &outgoingPacket{packet, ch}
// Lazily start background worker until we
// do our first write into the queue.
client.start.Do(func() {
go client.worker()
})
select {
case client.queue <- outgoingPacket:
default:
// Send would block, drop the packet
if client.DropHandler != nil {
client.DropHandler(packet)
}
ch <- ErrPacketDropped
client.wg.Done()
}
return packet.EventID, ch
}
// Capture asynchronously delivers a packet to the Sentry server with the default *Client.
// It is a no-op when client is nil. A channel is provided if it is important to check for a
// send's success.
func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
return DefaultClient.Capture(packet, captureTags)
}
// CaptureMessage formats and delivers a string message to the Sentry server.
func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
if client == nil {
return ""
}
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
eventID, _ := client.Capture(packet, tags)
return eventID
}
// CaptureMessage formats and delivers a string message to the Sentry server with the default *Client
func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
return DefaultClient.CaptureMessage(message, tags, interfaces...)
}
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
if client == nil {
return ""
}
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
eventID, ch := client.Capture(packet, tags)
<-ch
return eventID
}
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...)
}
// CaptureErrors formats and delivers an error to the Sentry server.
// Adds a stacktrace to the packet, excluding the call to this method.
func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
if client == nil {
return ""
}
packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...)
eventID, _ := client.Capture(packet, tags)
return eventID
}
// CaptureErrors formats and delivers an error to the Sentry server using the default *Client.
// Adds a stacktrace to the packet, excluding the call to this method.
func CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
return DefaultClient.CaptureError(err, tags, interfaces...)
}
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
if client == nil {
return ""
}
packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...)
eventID, ch := client.Capture(packet, tags)
<-ch
return eventID
}
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...)
}
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
// If an error is captured, both the error and the reported Sentry error ID are returned.
func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
// be completely noop though if we cared.
defer func() {
var packet *Packet
err = recover()
switch rval := err.(type) {
case nil:
return
case error:
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
default:
rvalStr := fmt.Sprint(rval)
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
}
errorID, _ = client.Capture(packet, tags)
}()
f()
return
}
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
// If an error is captured, both the error and the reported Sentry error ID are returned.
func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
return DefaultClient.CapturePanic(f, tags, interfaces...)
}
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
func (client *Client) CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
// be completely noop though if we cared.
defer func() {
var packet *Packet
err = recover()
switch rval := err.(type) {
case nil:
return
case error:
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
default:
rvalStr := fmt.Sprint(rval)
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
}
var ch chan error
errorID, ch = client.Capture(packet, tags)
<-ch
}()
f()
return
}
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
return DefaultClient.CapturePanicAndWait(f, tags, interfaces...)
}
func (client *Client) Close() {
close(client.queue)
}
func Close() { DefaultClient.Close() }
// Wait blocks and waits for all events to finish being sent to Sentry server
func (client *Client) Wait() {
client.wg.Wait()
}
// Wait blocks and waits for all events to finish being sent to Sentry server
func Wait() { DefaultClient.Wait() }
func (client *Client) URL() string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.url
}
func URL() string { return DefaultClient.URL() }
func (client *Client) ProjectID() string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.projectID
}
func ProjectID() string { return DefaultClient.ProjectID() }
func (client *Client) Release() string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.release
}
func Release() string { return DefaultClient.Release() }
func IncludePaths() []string { return DefaultClient.IncludePaths() }
func (client *Client) IncludePaths() []string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.includePaths
}
func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) }
func (client *Client) SetIncludePaths(p []string) {
client.mu.Lock()
defer client.mu.Unlock()
client.includePaths = p
}
func (c *Client) SetUserContext(u *User) { c.context.SetUser(u) }
func (c *Client) SetHttpContext(h *Http) { c.context.SetHttp(h) }
func (c *Client) SetTagsContext(t map[string]string) { c.context.SetTags(t) }
func (c *Client) ClearContext() { c.context.Clear() }
func SetUserContext(u *User) { DefaultClient.SetUserContext(u) }
func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) }
func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) }
func ClearContext() { DefaultClient.ClearContext() }
// HTTPTransport is the default transport, delivering packets to Sentry via the
// HTTP API.
type HTTPTransport struct {
*http.Client
}
func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error {
if url == "" {
return nil
}
body, contentType, err := serializedPacket(packet)
if err != nil {
return fmt.Errorf("error serializing packet: %v", err)
}
req, err := http.NewRequest("POST", url, body)
if err != nil {
return fmt.Errorf("can't create new request: %v", err)
}
req.Header.Set("X-Sentry-Auth", authHeader)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Content-Type", contentType)
res, err := t.Do(req)
if err != nil {
return err
}
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("raven: got http status %d", res.StatusCode)
}
return nil
}
func serializedPacket(packet *Packet) (io.Reader, string, error) {
packetJSON, err := packet.JSON()
if err != nil {
return nil, "", fmt.Errorf("error marshaling packet %+v to JSON: %v", packet, err)
}
// Only deflate/base64 the packet if it is bigger than 1KB, as there is
// overhead.
if len(packetJSON) > 1000 {
buf := &bytes.Buffer{}
b64 := base64.NewEncoder(base64.StdEncoding, buf)
deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression)
deflate.Write(packetJSON)
deflate.Close()
b64.Close()
return buf, "application/octet-stream", nil
}
return bytes.NewReader(packetJSON), "application/json", nil
}
var hostname string
func init() {
hostname, _ = os.Hostname()
}

41
vendor/github.com/getsentry/raven-go/exception.go generated vendored Normal file
View File

@@ -0,0 +1,41 @@
package raven
import (
"reflect"
"regexp"
)
var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`)
func NewException(err error, stacktrace *Stacktrace) *Exception {
msg := err.Error()
ex := &Exception{
Stacktrace: stacktrace,
Value: msg,
Type: reflect.TypeOf(err).String(),
}
if m := errorMsgPattern.FindStringSubmatch(msg); m != nil {
ex.Module, ex.Value = m[1], m[2]
}
return ex
}
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
type Exception struct {
// Required
Value string `json:"value"`
// Optional
Type string `json:"type,omitempty"`
Module string `json:"module,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
}
func (e *Exception) Class() string { return "exception" }
func (e *Exception) Culprit() string {
if e.Stacktrace == nil {
return ""
}
return e.Stacktrace.Culprit()
}

84
vendor/github.com/getsentry/raven-go/http.go generated vendored Normal file
View File

@@ -0,0 +1,84 @@
package raven
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"runtime/debug"
"strings"
)
func NewHttp(req *http.Request) *Http {
proto := "http"
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
proto = "https"
}
h := &Http{
Method: req.Method,
Cookies: req.Header.Get("Cookie"),
Query: sanitizeQuery(req.URL.Query()).Encode(),
URL: proto + "://" + req.Host + req.URL.Path,
Headers: make(map[string]string, len(req.Header)),
}
if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil {
h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
}
for k, v := range req.Header {
h.Headers[k] = strings.Join(v, ",")
}
return h
}
var querySecretFields = []string{"password", "passphrase", "passwd", "secret"}
func sanitizeQuery(query url.Values) url.Values {
for _, keyword := range querySecretFields {
for field := range query {
if strings.Contains(field, keyword) {
query[field] = []string{"********"}
}
}
}
return query
}
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
type Http struct {
// Required
URL string `json:"url"`
Method string `json:"method"`
Query string `json:"query_string,omitempty"`
// Optional
Cookies string `json:"cookies,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Env map[string]string `json:"env,omitempty"`
// Must be either a string or map[string]string
Data interface{} `json:"data,omitempty"`
}
func (h *Http) Class() string { return "request" }
// Recovery handler to wrap the stdlib net/http Mux.
// Example:
// http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) {
// ...
// }))
func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rval := recover(); rval != nil {
debug.PrintStack()
rvalStr := fmt.Sprint(rval)
packet := NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r))
Capture(packet, nil)
w.WriteHeader(http.StatusInternalServerError)
}
}()
handler(w, r)
}
}

49
vendor/github.com/getsentry/raven-go/interfaces.go generated vendored Normal file
View File

@@ -0,0 +1,49 @@
package raven
// https://docs.getsentry.com/hosted/clientdev/interfaces/#message-interface
type Message struct {
// Required
Message string `json:"message"`
// Optional
Params []interface{} `json:"params,omitempty"`
}
func (m *Message) Class() string { return "logentry" }
// https://docs.getsentry.com/hosted/clientdev/interfaces/#template-interface
type Template struct {
// Required
Filename string `json:"filename"`
Lineno int `json:"lineno"`
ContextLine string `json:"context_line"`
// Optional
PreContext []string `json:"pre_context,omitempty"`
PostContext []string `json:"post_context,omitempty"`
AbsolutePath string `json:"abs_path,omitempty"`
}
func (t *Template) Class() string { return "template" }
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
type User struct {
// All fields are optional
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
IP string `json:"ip_address,omitempty"`
}
func (h *User) Class() string { return "user" }
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
type Query struct {
// Required
Query string `json:"query"`
// Optional
Engine string `json:"engine,omitempty"`
}
func (q *Query) Class() string { return "query" }

4
vendor/github.com/getsentry/raven-go/runtests.sh generated vendored Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
go test -race ./...
go test -cover ./...
go test -v ./...

213
vendor/github.com/getsentry/raven-go/stacktrace.go generated vendored Normal file
View File

@@ -0,0 +1,213 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Some code from the runtime/debug package of the Go standard library.
package raven
import (
"bytes"
"go/build"
"io/ioutil"
"path/filepath"
"runtime"
"strings"
"sync"
)
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
type Stacktrace struct {
// Required
Frames []*StacktraceFrame `json:"frames"`
}
func (s *Stacktrace) Class() string { return "stacktrace" }
func (s *Stacktrace) Culprit() string {
for i := len(s.Frames) - 1; i >= 0; i-- {
frame := s.Frames[i]
if frame.InApp == true && frame.Module != "" && frame.Function != "" {
return frame.Module + "." + frame.Function
}
}
return ""
}
type StacktraceFrame struct {
// At least one required
Filename string `json:"filename,omitempty"`
Function string `json:"function,omitempty"`
Module string `json:"module,omitempty"`
// Optional
Lineno int `json:"lineno,omitempty"`
Colno int `json:"colno,omitempty"`
AbsolutePath string `json:"abs_path,omitempty"`
ContextLine string `json:"context_line,omitempty"`
PreContext []string `json:"pre_context,omitempty"`
PostContext []string `json:"post_context,omitempty"`
InApp bool `json:"in_app"`
}
// Intialize and populate a new stacktrace, skipping skip frames.
//
// context is the number of surrounding lines that should be included for context.
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
// one line with no surrounding context, and 0 returns no context.
//
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
var frames []*StacktraceFrame
for i := 1 + skip; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
if frame != nil {
frames = append(frames, frame)
}
}
// If there are no frames, the entire stacktrace is nil
if len(frames) == 0 {
return nil
}
// Optimize the path where there's only 1 frame
if len(frames) == 1 {
return &Stacktrace{frames}
}
// Sentry wants the frames with the oldest first, so reverse them
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
return &Stacktrace{frames}
}
// Build a single frame using data returned from runtime.Caller.
//
// context is the number of surrounding lines that should be included for context.
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
// one line with no surrounding context, and 0 returns no context.
//
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
frame.Module, frame.Function = functionName(pc)
// `runtime.goexit` is effectively a placeholder that comes from
// runtime/asm_amd64.s and is meaningless.
if frame.Module == "runtime" && frame.Function == "goexit" {
return nil
}
if frame.Module == "main" {
frame.InApp = true
} else {
for _, prefix := range appPackagePrefixes {
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
frame.InApp = true
}
}
}
if context > 0 {
contextLines, lineIdx := fileContext(file, line, context)
if len(contextLines) > 0 {
for i, line := range contextLines {
switch {
case i < lineIdx:
frame.PreContext = append(frame.PreContext, string(line))
case i == lineIdx:
frame.ContextLine = string(line)
default:
frame.PostContext = append(frame.PostContext, string(line))
}
}
}
} else if context == -1 {
contextLine, _ := fileContext(file, line, 0)
if len(contextLine) > 0 {
frame.ContextLine = string(contextLine[0])
}
}
return frame
}
// Retrieve the name of the package and function containing the PC.
func functionName(pc uintptr) (pack string, name string) {
fn := runtime.FuncForPC(pc)
if fn == nil {
return
}
name = fn.Name()
// We get this:
// runtime/debug.*T·ptrmethod
// and want this:
// pack = runtime/debug
// name = *T.ptrmethod
if idx := strings.LastIndex(name, "."); idx != -1 {
pack = name[:idx]
name = name[idx+1:]
}
name = strings.Replace(name, "·", ".", -1)
return
}
var fileCacheLock sync.Mutex
var fileCache = make(map[string][][]byte)
func fileContext(filename string, line, context int) ([][]byte, int) {
fileCacheLock.Lock()
defer fileCacheLock.Unlock()
lines, ok := fileCache[filename]
if !ok {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, 0
}
lines = bytes.Split(data, []byte{'\n'})
fileCache[filename] = lines
}
line-- // stack trace lines are 1-indexed
start := line - context
var idx int
if start < 0 {
start = 0
idx = line
} else {
idx = context
}
end := line + context + 1
if line >= len(lines) {
return nil, 0
}
if end > len(lines) {
end = len(lines)
}
return lines[start:end], idx
}
var trimPaths []string
// Try to trim the GOROOT or GOPATH prefix off of a filename
func trimPath(filename string) string {
for _, prefix := range trimPaths {
if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
return trimmed
}
}
return filename
}
func init() {
// Collect all source directories, and make sure they
// end in a trailing "separator"
for _, prefix := range build.Default.SrcDirs() {
if prefix[len(prefix)-1] != filepath.Separator {
prefix += string(filepath.Separator)
}
trimPaths = append(trimPaths, prefix)
}
}

20
vendor/github.com/getsentry/raven-go/writer.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
package raven
type Writer struct {
Client *Client
Level Severity
Logger string // Logger name reported to Sentry
}
// Write formats the byte slice p into a string, and sends a message to
// Sentry at the severity level indicated by the Writer w.
func (w *Writer) Write(p []byte) (int, error) {
message := string(p)
packet := NewPacket(message, &Message{message, nil})
packet.Level = w.Level
packet.Logger = w.Logger
w.Client.Capture(packet, nil)
return len(p), nil
}

57
vendor/github.com/go-sql-driver/mysql/AUTHORS generated vendored Normal file
View File

@@ -0,0 +1,57 @@
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
# If you are submitting a patch, please add your name or the name of the
# organization which holds the copyright to this list in alphabetical order.
# Names should be added to this file as
# Name <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Arne Hormann <arnehormann at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
INADA Naoki <songofacandy at gmail.com>
James Harr <james.harr at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Olivier Mengué <dolmen at cpan.org>
Paul Bonser <misterpib at gmail.com>
Runrioter Wung <runrioter at gmail.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Zhenye Xie <xiezhenye at gmail.com>
# Organizations
Barracuda Networks, Inc.
Google Inc.
Stripe Inc.

119
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,119 @@
## Version 1.3 (2016-12-01)
Changes:
- Go 1.1 is no longer supported
- Use decimals fields in MySQL to format time types (#249)
- Buffer optimizations (#269)
- TLS ServerName defaults to the host (#283)
- Refactoring (#400, #410, #437)
- Adjusted documentation for second generation CloudSQL (#485)
- Documented DSN system var quoting rules (#502)
- Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
New Features:
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
- Support for returning table alias on Columns() (#289, #359, #382)
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
- Support for uint64 parameters with high bit set (#332, #345)
- Cleartext authentication plugin support (#327)
- Exported ParseDSN function and the Config struct (#403, #419, #429)
- Read / Write timeouts (#401)
- Support for JSON field type (#414)
- Support for multi-statements and multi-results (#411, #431)
- DSN parameter to set the driver-side max_allowed_packet value manually (#489)
- Native password authentication plugin support (#494, #524)
Bugfixes:
- Fixed handling of queries without columns and rows (#255)
- Fixed a panic when SetKeepAlive() failed (#298)
- Handle ERR packets while reading rows (#321)
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
- Actually zero out bytes in handshake response (#378)
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
- Fixed tests with MySQL 5.7.9+ (#380)
- QueryUnescape TLS config names (#397)
- Fixed "broken pipe" error by writing to closed socket (#390)
- Fixed LOAD LOCAL DATA INFILE buffering (#424)
- Fixed parsing of floats into float64 when placeholders are used (#434)
- Fixed DSN tests with Go 1.7+ (#459)
- Handle ERR packets while waiting for EOF (#473)
- Invalidate connection on error while discarding additional results (#513)
- Allow terminating packets of length 0 (#516)
## Version 1.2 (2014-06-03)
Changes:
- We switched back to a "rolling release". `go get` installs the current master branch again
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
- Exported errors to allow easy checking from application code
- Enabled TCP Keepalives on TCP connections
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
- The DSN parser also checks for a missing separating slash
- Faster binary date / datetime to string formatting
- Also exported the MySQLWarning type
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
- writePacket() automatically writes the packet size to the header
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
New Features:
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
- Logging of critical errors is configurable with `SetLogger`
- Google CloudSQL support
Bugfixes:
- Allow more than 32 parameters in prepared statements
- Various old_password fixes
- Fixed TestConcurrent test to pass Go's race detection
- Fixed appendLengthEncodedInteger for large numbers
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
## Version 1.1 (2013-11-02)
Changes:
- Go-MySQL-Driver now requires Go 1.1
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
- Optimized the buffer for reading
- stmt.Query now caches column metadata
- New Logo
- Changed the copyright header to include all contributors
- Improved the LOAD INFILE documentation
- The driver struct is now exported to make the driver directly accessible
- Refactored the driver tests
- Added more benchmarks and moved all to a separate file
- Other small refactoring
New Features:
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
Bugfixes:
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
- Convert to DB timezone when inserting `time.Time`
- Splitted packets (more than 16MB) are now merged correctly
- Fixed false positive `io.EOF` errors when the data was fully read
- Avoid panics on reuse of closed connections
- Fixed empty string producing false nil values
- Fixed sign byte for positive TIME fields
## Version 1.0 (2013-05-14)
Initial Release

23
vendor/github.com/go-sql-driver/mysql/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,23 @@
# Contributing Guidelines
## Reporting Issues
Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed).
## Contributing Code
By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file.
Don't forget to add yourself to the AUTHORS file.
### Code Review
Everyone is invited to review and comment on pull requests.
If it looks fine to you, comment with "LGTM" (Looks good to me).
If changes are required, notice the reviewers with "PTAL" (Please take another look) after committing the fixes.
Before merging the Pull Request, at least one [team member](https://github.com/go-sql-driver?tab=members) must have commented with "LGTM".
## Development Ideas
If you are looking for ideas for code contributions, please check our [Development Ideas](https://github.com/go-sql-driver/mysql/wiki/Development-Ideas) Wiki page.

Some files were not shown because too many files have changed in this diff Show More