Compare commits

...
193 Commits
Author SHA1 Message Date
gisle 6ad0b9d467 fix character service not refreshing logs. 2020-10-07 20:33:56 +02:00
gisle 63bc262091 add sortByFictionalDate field to Story, and sorting by fictional date if set.
continuous-integration/drone/push Build is passing
2020-08-28 22:45:13 +02:00
gisle c4eabd69ac Merge branch 'master' of ssh+git://git.aiterp.net/rpdata/api
continuous-integration/drone/push Build is passing
2020-08-20 21:01:38 +02:00
gisle df982fac17 add IrcCloud log importer. 2020-08-20 20:58:16 +02:00
gisle 6fa2dc9800 fix chapter edit changes being reported as comment add. 2020-08-19 21:26:56 +02:00
gisle 6ce38d61b1 fix log importer not refreshing character list.
continuous-integration/drone/push Build is passing
2020-08-16 11:49:39 +02:00
gisle dc3f77f8da update drone ci.
continuous-integration/drone/push Build is passing
2020-08-09 16:09:11 +02:00
gisle 6321ea1893 update drone ci.
continuous-integration/drone/push Build is failing
2020-08-09 16:08:11 +02:00
gisle 9b346fb97b update drone ci.
continuous-integration/drone/push Build is passing
2020-08-09 16:07:44 +02:00
gisle ed21b7c8cd improve parser error. 2020-08-09 16:01:45 +02:00
gisle 47b7877ab4 add nextLogs and prevLogs suggestions to log. 2020-08-09 15:35:49 +02:00
gisle 757b0bc616 add a hackish migration for the changes expiration index. 2020-08-09 15:35:33 +02:00
gisle 5dbed6d632 UI for file stuff.
the build was successful
2019-09-15 16:34:16 +02:00
gisle c8ced52e1a File uploads, mostly. Also added extra behavior to addCharacter.
the build was successful
2019-08-31 22:46:55 +02:00
gisle 8a313b3bb2 More destruction. Utils too, since they cause weird go mod shenanigans.
the build was successful
2019-08-26 22:22:05 +02:00
gisle e690e7ab97 A lot of delete and failed scream tests.
the build was successful
2019-08-26 22:14:59 +02:00
gisle 0e70e37038 Moved unknown nicks to log service
the build was successful
2019-08-26 20:23:36 +02:00
gisle 3b10c4d9b1 go mod tidy
the build was successful
2019-08-25 16:14:29 +02:00
gisle 690eb4f121 Deleted a bunch of old db code.
the build was successful
2019-08-25 16:13:04 +02:00
gisle 2eed815993 Finished refactor of story/chapter/comment related stuff.
the build was successful
2019-08-25 16:10:10 +02:00
gisle 8c4670580c Story/chapter/comment repos + started on story service.
the build was successful
2019-08-18 08:59:38 +02:00
gisle 8fbaea2078 Added mergeLog mutation.
the build was successful
2019-08-16 12:32:50 +02:00
gisle 2a0e6534ff Added splitLog mutation.
the build was successful
2019-08-15 11:28:09 +02:00
gisle 4bd75b8240 Fixed character filter not being exclusive. 2019-08-15 11:28:02 +02:00
gisle ed38d576b2 Added fix for date bug.
the build was successful
2019-08-14 22:01:48 +02:00
gisle 51b2c9bfe0 Fixed log service bugs with importing and character list not updating.
the build was successful
2019-08-14 11:28:02 +02:00
gisle c0687d76ad Added log character refresh function to log service.
the build was successful
2019-08-07 12:26:59 +02:00
gisle 60dcd8a80f no idea
the build was successful
2019-08-06 13:51:51 +02:00
gisle 5bcc7479ff Early work on file stuff.
the build was successful
2019-07-25 23:21:25 +02:00
gisle 2c00358f4f Fixed advance by one date bug by treating any out of order posts as a date bump.
the build was successful
2019-07-20 15:20:39 +02:00
gisle acb6b637a5 Moved channels to new system, fixed change insert not working.
the build was successful
2019-06-24 20:30:14 +02:00
gisle f46b6f0774 Added channel repository and mongodb impl.
the build was successful
2019-06-20 22:32:35 +02:00
gisle 73a1c79903 Logs and posts are now on the new architecture.
the build was successful
2019-06-18 20:34:58 +02:00
gisle e194b6536e Removed PassAll from ChangeFilter and fixed sort not being descending order.
the build was successful
2019-06-16 11:46:23 +02:00
gisle f6ad0602c2 Replaced repositories.Bundle with a database interface.
the build was successful
2019-06-16 11:39:33 +02:00
gisle 021f67ea4b Changes to change system.
the build was successful
2019-06-16 10:32:34 +02:00
gisle 13f82399ef Removed ./idea/workspace.xml from git.
the build was successful
2019-06-10 19:29:38 +02:00
gisle 2a1423d28f Added character service and such.
the build was successful
2019-06-10 17:03:52 +02:00
gisle 7b253748fe Replaced custom Date scalar code with gqlgen's built-in Time scalar.
the build was successful
2019-06-09 12:49:18 +02:00
gisle 53c3d4597b Renamed queries package to resolvers.
the build was successful
2019-06-09 12:40:40 +02:00
gisle cdf4e6f667 Started work on new layered setup, making a huge mess.
the build was successful
2019-06-09 12:37:17 +02:00
gisle 6fa9f64f2d Instrumented the living s*** out of the graphql.
the build was successful
2019-05-21 19:35:29 +02:00
gisle 2fb391321e Fixed bug in resolver array conversion that caused results to all be copied of the last element. 2019-05-21 19:35:11 +02:00
gisle 015bdfd2e8 Updated resolvers to be compatible with gqlgen 0.9
the build was successful
2019-05-19 17:06:46 +02:00
gisle 9925907e93 Removed combine.sh, using new glob schema features instead.
the build was successful
2019-05-17 17:31:42 +02:00
gisle c7c418aeab Fixed Change.object resolver not returning anything. 2019-05-17 17:12:56 +02:00
gisle d5573e7f39 Added Prometheus metrics, don't forget to restrict access on /metrics endpoint. 2019-05-17 17:12:27 +02:00
gisle e794311eb8 Migrated rpdata-api to using go modules and a more recent version of
the build was successful
gqlgen

Squashed commit of the following:

commit 162ccf6bfb
Author: Gisle Aune <dev@gisle.me>
Date:   Fri May 17 11:51:23 2019 +0200

    The great module madness continus, now with pointers.

commit 7e3dc49635
Author: Gisle Aune <dev@gisle.me>
Date:   Thu May 16 15:57:08 2019 +0200

    Basic stuff, doesn't compile
2019-05-17 12:20:08 +02:00
gisle aff0d99842 graph2: Added exception for open logs query in complexity.
the build was successful
2019-04-27 11:37:01 +02:00
gisle 00150118d2 graph2: Added complexity to prevent queries that can take down the server.
the build was successful
2019-04-25 20:47:20 +02:00
gisle 332a1eb32b Fixed rpdata-as2storyimport not compiling. 2019-03-17 21:32:21 +01:00
gisle db4d46c368 Added story to comment changes.
the build was successful
2019-03-17 20:44:30 +01:00
gisle a8c8610ca2 models: Fixed wrong database mapping for comment source.
the build was successful
2019-01-17 21:19:13 +01:00
gisle d09c17d30d graph2: Changed addComment error message on can-comment check to be consistent with others.
the build was successful
2019-01-16 22:07:00 +01:00
gisle e3e3ea07db graph2: Changed Chapter to not return any comments if mode is disabled, even if there are comments.
the build was successful
2019-01-16 22:05:40 +01:00
gisle 22592439da graph2, models: Added removeComment mutation.
the build was successful
2019-01-16 22:03:28 +01:00
gisle dfa9fb4025 graph2: Changed Comment to return null instead of a zero-date if it's cleared.
the build was successful
2019-01-16 21:50:53 +01:00
gisle 0e2a68a2d4 graph2, models: Added editComment mutation. 2019-01-16 21:45:00 +01:00
gisle 1eefa65527 models: Fixed wrong database mapping for comment id. 2019-01-16 21:41:36 +01:00
gisle f9b6af58c9 graph2: Changed Comment character resolver to use loader.
the build was successful
2019-01-15 21:42:44 +01:00
gisle 56ecf48b57 graph2, models: Added addComment mutation.
the build was successful
2019-01-15 21:34:57 +01:00
gisle 06fbfc90c5 graph2, model: Added Comment type to Change. 2019-01-14 19:58:05 +01:00
gisle 39a4fef506 models, graph2: Added comment removal on chapter removal.
the build was successful
2019-01-14 18:01:43 +01:00
gisle f9fe0bb5f8 models, graph2: Added comment options to chapter mutations.
the build was successful
2019-01-14 17:55:38 +01:00
gisle d22ab387dd models, graph2: Added Comment type and queries, and hooked it up to chapter.
the build was successful
2019-01-14 17:03:16 +01:00
gisle 53a86c0b9c store, config: Added option to disable space.
the build was successful
2019-01-12 12:13:24 +01:00
gisle e0b93c6b4b graph2: Changed story tagging permissions to allow anyone to tag open stories.
the build was successful
2019-01-12 11:38:19 +01:00
gisle 1ea04f1577 importers: Fixed panic in mirclike.ParsePost if the post contains no text.
the build was successful
2019-01-05 10:33:50 +01:00
gisle 17fd3c60f8 rpdata-server: Fixed unhelpful error message being returned on panic.
the build was successful
2019-01-05 10:09:31 +01:00
gisle c5b62a42db graph2: Fixed data race in change logging for importLog.
the build was successful
2018-12-11 20:05:03 +01:00
gisle fa0926c311 rpdata-lb2logimport: Updated it to fix build errors related to change in logs.UpdateChracters.
the build was successful
2018-12-08 16:08:28 +01:00
gisle d21efc1c7f rpdata-logpaste: Removed the command since it's replaced by GraphQL API endpoint. 2018-12-08 16:07:44 +01:00
gisle 852030b141 docker: Trimmed down DockerFile. 2018-12-08 16:07:11 +01:00
gisle f7f0b76379 changes: Reversed ordering in result.
the build was successful
2018-12-05 21:00:52 +01:00
gisle 2711654816 posts: Fixed AddMany not returning the IDs in the result.
the build was successful
2018-12-05 20:58:37 +01:00
gisle b60897a597 unknownnicks, logs: Changed unknownnicks counter to use posts instead of logs as a metric to make characters stick out more.
the build was successful
2018-12-04 20:49:31 +01:00
gisle 80e6c048c9 graph2: Added unknownNick query
the build was successful
2018-12-04 20:33:21 +01:00
gisle 6310cbbc1e unknownnicks: Added unknown nicks model and hooked it up. 2018-12-04 20:17:48 +01:00
gisle 89a1c2366c changes: Fixed crash on disconnect when there are more than one client connected. 2018-12-03 20:11:15 +01:00
gisle ac2b79e88a posts, counter: Fixed post positions issues when a post is deleted.
the build was successful
2018-12-02 21:27:50 +01:00
gisle 31239330df graph2, changes, rpdata-server: Added changes subscription. 2018-12-02 21:17:49 +01:00
gisle a58535fcb9 graph2: Added importLog query and associated models and repository functions.
the build was successful
2018-12-01 16:52:06 +01:00
gisle 2a91b6f9bc logs: Added forumlog parsers.
the build was successful
2018-12-01 12:48:05 +01:00
gisle caa7727c02 logs: Added mirclike parsers.
the build was successful
2018-11-28 19:21:53 +01:00
gisle 047c9784ae rpdata-logpaste: Fixed characters not being updated.
the build was successful
2018-11-07 19:56:56 +01:00
gisle d038c1cfea Dockerfile: Added tzdata package to final container.
the build was successful
2018-11-07 19:48:03 +01:00
gisle 7e812bd615 rpdata-logpaste: Added new commandline tool.
the build was successful
2018-11-07 19:31:51 +01:00
gisle 947765884e models: Fixed earliestDate being exclusive of the earliest date in chnages.List.
the build was successful
2018-11-01 19:38:01 +01:00
gisle ffec008820 graph2: Added change type and query.
the build was successful
2018-10-31 21:38:45 +01:00
gisle 138b3e3eab graph2: Fixed post change on move not including all posts as objects. 2018-10-31 21:38:32 +01:00
gisle 929abfdbe4 package: Removed rpdata-ui. 2018-10-31 20:28:09 +01:00
gisle a8cd595876 graph2: Added change logging for story and chapter, fixed channel ops not being logged.
the build was successful
2018-10-29 19:12:23 +01:00
gisle 9ed3b32b42 models: Added support for multiple on changekeys.All, though only first is used for * key. 2018-10-29 17:51:39 +01:00
gisle 6bae52d652 models: Added benchmarks to changekeys, added shortcuts for common types there could be many of. 2018-10-29 17:44:48 +01:00
gisle cee370c2ac models: Changed change logging system to be allow for better querying in the future.
the build was successful
2018-10-28 12:18:17 +01:00
gisle 9affdbd20e graph2: Added move mutation to chapter.
the build was successful
2018-10-27 14:59:47 +02:00
gisle cf545cde3b graphql: Removed old graphql setup.
the build was successful
2018-10-27 10:16:25 +02:00
gisle b414cf118f rpdata-wikifileimport: Fixed it to use new models module.
the build was successful
2018-10-27 10:15:23 +02:00
gisle 4979b02104 models: Migrated last from model 2018-10-27 10:14:58 +02:00
gisle b7d5059d60 graph2: Fixed chapter using story permissions.
the build was successful
2018-10-16 19:01:11 +02:00
gisle 9fbdc767ad models2: Added 'post.remove' to default user permissionss.
the build was successful
2018-10-16 18:49:14 +02:00
gisle daeaaf5fa9 rpdata-as2storyimport: Fixed category vailidty check failing when it is valid, not when it isn't.
the build was successful
2018-10-16 18:30:24 +02:00
gisle d3277c7476 models2: Fixed text search not working.
the build was successful
2018-10-13 14:04:02 +02:00
gisle 90681c67b0 models2: Added post removal on log removal. 2018-10-13 11:43:53 +02:00
gisle bebd99833b models2: Fixed incorrect db mappings, fixed Edit not returning mutated model. 2018-10-11 22:26:08 +02:00
gisle 34f86d5037 graph2: Added logging changes for Channel, Log, Post and Character.
the build failed
2018-10-09 20:25:55 +02:00
gisle 8a85313762 docker: Updated DockerFile.
the build failed
2018-10-09 19:33:51 +02:00
gisle 017e68eae7 rpdata-ensurechannels: Ported to use new models package.
the build failed
2018-10-07 14:48:23 +02:00
gisle 1955ad0517 rpdata-lb2logimport: Changed script to use new models package.
the build failed
2018-10-07 14:45:44 +02:00
gisle 34ba5d5d3a models2: Fixed issue caused by last commit removing packages that were still barely in use. 2018-10-07 13:41:39 +02:00
gisle 8c8a251080 package: Removed old models packages that are ported over to the new format.
the build failed
2018-10-07 12:18:55 +02:00
gisle 73fe125870 models2: Added log character updates.
the build failed
2018-10-07 12:16:53 +02:00
gisle 04ec85aadf graph2: Removed limit on posts returned for Log.posts field. 2018-10-07 12:16:12 +02:00
gisle 59233ee549 models2: Fixed characters using wrong auto increment
the build failed
2018-10-07 10:56:29 +02:00
gisle 7aa0de9135 models2: Fixed error caused by empty change set in edit
the build failed
2018-10-06 14:02:39 +02:00
gisle 6dfbee7ff2 models2: Fixed editing characters erasing all unedited data.
the build failed
2018-10-06 13:55:16 +02:00
gisle 8a07c2ffdb graph2: Remove CharactersFilter logged property.
the build failed
2018-10-06 10:53:15 +02:00
gisle 0e9536b0a8 models2: Fixed StoryCategory UnmarshalGQL validating before writing the value.
the build failed
2018-10-03 20:56:18 +02:00
gisle b8e5ed5813 graph2: Added channel mutations.
the build failed
2018-10-01 22:20:52 +02:00
gisle 2317f66920 graph2: Added character mutations
the build failed
2018-10-01 22:05:12 +02:00
gisle fc488b938d graph2: Fixed last change closing all logs upon opening one, even the one just opened.
the build failed
2018-10-01 21:01:43 +02:00
gisle ceb2617571 graph2: Added closing of all other logs in the same channel if a log is opened.
the build failed
2018-10-01 20:22:37 +02:00
gisle aeb5b26627 graph2: Added Log mutations
the build failed
2018-09-30 17:05:26 +02:00
gisle 6aafc67790 rpdata-server: >_>
the build failed
2018-09-30 11:28:25 +02:00
gisle c6f8fd9faa git: readme
the build failed
2018-09-30 11:14:01 +02:00
gisle efa98b80cc rpdata-as2storyimport: Changed to use new models.
the build failed
2018-09-29 16:38:36 +02:00
gisle 707dcb43eb models2: Added IsValid() method to StoryCategory 2018-09-29 16:38:21 +02:00
gisle 9bcc28a0ea rpdata-graphiql: removed command
the build failed
2018-09-29 16:21:36 +02:00
gisle 2fceaa62ed rpdata-lb2charimport: Ported to using new models package
the build failed
2018-09-29 16:20:36 +02:00
gisle 86125c7ce9 graph2: Added add/remove nick mutations to character. 2018-09-29 16:20:08 +02:00
gisle bb28795cd5 graph2: Added post mutations. general: Did some cleaning.
the build failed
2018-09-29 15:54:22 +02:00
gisle 815f30597e graph2: Added removal of chapters upon story being deleted.
the build failed
2018-09-23 13:53:13 +02:00
gisle cd1f122502 graph2: Fixed inverse permission checks in story mutations.
the build failed
2018-09-22 15:31:53 +02:00
gisle 0e0d802a32 graph2: Added Story mutations
the build failed
2018-09-20 20:50:22 +02:00
gisle 3c42043ab6 models: Added Equals method to tag model. 2018-09-20 20:49:57 +02:00
gisle 8388ee2446 graph2: Added clearFictionalDate option to editChapter mutation input
the build failed
2018-09-19 22:19:39 +02:00
gisle 260c2e7f76 auth: Changed token checking to ignore disallowed permissions instead of invalidate the key
the build failed
2018-09-18 20:24:04 +02:00
gisle a4034b0faa graph2: Added Chapter mutations
the build failed
2018-09-17 20:11:57 +02:00
gisle c3a6798050 rpdata-server: Changed endpoint from /query to /graphql 2018-09-16 23:52:45 +02:00
gisle 2ab933149e auth: Moved token and user to models package, added graphql endpoint to check token
the build failed
2018-09-16 16:57:40 +02:00
gisle 07b94b3cab auth: Added list of permissions
the build failed
2018-09-16 12:55:28 +02:00
gisle 79171bf9a2 graph2: Added Story type and queries. 2018-09-16 12:55:14 +02:00
gisle bac57bc378 graph2: Added File type and queries.
the build failed
2018-09-16 11:30:18 +02:00
gisle 875dac9ecb graph2: Added Chapter type and queries, moved loader out of old graphql pkg.
the build failed
2018-09-16 10:33:55 +02:00
gisle 0768cfd0bd models: Painstakingly converted half the model package to a models & repostiry stucture. All binaries are probably dead at this point.
the build failed
2018-09-16 00:13:12 +02:00
gisle 2b05d90f4a graph2: Added Log type and queries, moved loader out of old graphql pkg.
the build failed
2018-09-15 13:14:05 +02:00
gisle 0444911e73 graph2: Added Post type and queries 2018-09-15 09:29:04 +02:00
gisle 1be4088a8f graph2: Added Channel type and queries
the build failed
2018-09-13 22:49:36 +02:00
gisle 9978d7380b graph, rpdata-server: Started on new GraphQL implementation.
the build failed
2018-09-13 21:13:05 +02:00
gisle 2e5162a29b auth: Fixed incorrect error reporting on permissions mismatching, added ensuring of user in auth.FindUser.
the build failed
2018-09-13 20:08:37 +02:00
gisle dac0060155 auth: Replaced old system with api key-based system.
the build failed
2018-09-12 21:13:14 +02:00
gisle 9d7fd498a9 Auth: Removed session/auth system from GraphQL API 2018-09-07 17:59:57 +02:00
gisle e0af2685ee Logs: Moved filtering logic out of GraphQL query resolver to the Log model.
the build failed
2018-09-02 13:46:12 +02:00
gisle 3184b60310 GraphQL: Optimized dataloaders by adding a 'priming' mechanic to make fewer trips to the database.
the build failed
2018-09-02 12:38:17 +02:00
gisle 675c09ef04 git: removed binary file from project root
the build failed
2018-08-30 20:32:05 +02:00
gisle 9707320960 git: updated gitignore
the build failed
2018-08-30 20:29:50 +02:00
gisle 20e53b4f5e GraphQL: Fixed stories resolver crashing if filter is nil.
the build failed
2018-08-30 20:21:00 +02:00
gisle 8bb4dea358 UI: Started on story content 2018-08-25 10:35:29 +02:00
gisle 75b2382f09 Changed colors to more closely match aitestory
the build was successful
2018-08-01 22:26:18 +02:00
gisle 1ca25eff8e Fixed warnings
the build was successful
2018-08-01 21:49:58 +02:00
gisle bf10ed1fe3 Added ListItem and changed StoryList ot use it, reorganized StoryMenu, added scrollbar styling, added moment dependency
the build was successful
2018-08-01 18:02:01 +02:00
gisle 54136be912 Removed leftover console.log()s 2018-08-01 15:51:38 +02:00
gisle 228c2bfeb4 Removed docker build on non-tag push from CI
the build was successful
2018-08-01 15:47:27 +02:00
gisle 2f98be5747 Removed default limit for stories query
the build was successful
2018-08-01 15:19:08 +02:00
gisle 2a40f97282 Added order to stories query response
the build was successful
2018-08-01 15:04:16 +02:00
gisle e5b4184106 Fixed query for category lists not getting parameters 2018-08-01 14:57:49 +02:00
gisle c348a90b70 Fixed nil deref error on no filter, fixed no results
the build was successful
2018-08-01 14:51:45 +02:00
gisle 132cf28f0f Added category to StoriesFilter
the build was successful
2018-08-01 14:38:09 +02:00
gisle d5df624188 Added open and unlsited to StoriesFilter
the build was successful
2018-08-01 14:27:28 +02:00
gisle ca98648274 Added router for story menu and queries for the lists 2018-08-01 14:07:54 +02:00
gisle 4642a8abec Added missing dpeencency (apollo-cache-inmemory), changed proxy to work within the local network.
the build was successful
2018-08-01 11:23:15 +02:00
gisle 0ec923d686 Added logged filter to characters query: character appears in logs.
the build was successful
2018-07-18 00:13:19 +02:00
gisle 388d5378e7 Cleaned up the ui code, addressed warnings
the build was successful
2018-07-16 20:24:01 +02:00
gisle 67e9e8f1e6 Renamed LogsIndex -> LogsRoot, removed hardcoded bg image from LogsRoot
the build was successful
2018-07-15 20:08:32 +02:00
gisle 06bc7e5a1b Updated StoryRoot graphql query according to api changes 2018-07-15 19:59:23 +02:00
gisle 237190e498 Added LoadingScreen for full-size loading placeholder 2018-07-15 19:58:12 +02:00
gisle b9f0fef966 Fixed Background landscape detection always returning the intiial result, reacitified the Background class 2018-07-15 19:57:28 +02:00
gisle ba1c1e0a4b Changed characters graphql query to use the filter format, changed description for files query to match the others'
the build was successful
2018-07-15 17:55:21 +02:00
gisle 18e77b8205 Changed .gitignore to include new bindata.go location 2018-07-15 17:14:35 +02:00
gisle d8871259a1 Changed channels graphql query to use filter, added eventName and locationName filters 2018-07-15 16:58:17 +02:00
gisle 26d7e36631 Changed files graphql query to use a filter argument to look more like other queries. 2018-07-15 15:34:58 +02:00
gisle 51489ed901 Changed log query's ID parameter to be compulsory 2018-07-15 15:16:47 +02:00
gisle ed3e384d55 Moved query and mutation resolvers into individual files
the build was successful
2018-07-15 15:10:35 +02:00
gisle 9455fd960f Moved packages loader, resolver and schema under graphql
the build was successful
2018-07-15 14:10:00 +02:00
gisle 36939ffc59 Moved SessionResolver and UserResolver into resolvers/types package, updated references to it.
the build was successful
2018-07-14 19:14:56 +02:00
gisle 5fedce5899 Moved StoryResolver and TagResolver into resolvers/types package, updated references to it. 2018-07-14 19:13:43 +02:00
gisle dced5282b1 Moved LogResolver and PostResolver into resolvers/types package, updated references to it. 2018-07-14 19:11:04 +02:00
gisle d05f3a8ca8 Moved FileResolver into resolvers/types package, updated references to it.
the build was successful
2018-07-14 18:39:34 +02:00
gisle 53e2dfc6b6 Moved CharacterResolver into resolvers/types package, updated references to it. 2018-07-14 18:37:47 +02:00
gisle fba499024b Moved ChapterResolver into resolvers/types package, updated references to it. 2018-07-14 18:36:08 +02:00
gisle cc765e220a Moved ChannelResolver into resolvers/types package, updated references to it. 2018-07-14 18:33:37 +02:00
gisle ba49b1fd30 Renamed input parameter in stories and logs to filter, made filter optional for stories query 2018-07-14 18:29:00 +02:00
gisle 8c290c8dcd Removed hard-coded sample tags 2018-07-12 22:15:12 +02:00
196 changed files with 10529 additions and 18806 deletions
+34 -16
View File
@@ -1,21 +1,39 @@
workspace:
base: /go
path: src/git.aiterp.net/rpdata/api
name: rpdata-api
pipeline:
docker-release:
kind: pipeline
type: docker
steps:
- name: go-test
image: golang:1.14
commands:
- go generate ./...
- go test -v ./...
- name: docker-release-tag
image: plugins/docker
repo: vmaple/rpdata
tags: latest
auto_tag: true
secrets: [ docker_username, docker_password ]
settings:
auto_tag: true
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-api
registry: r.vmaple.dev
when:
event: tag
ref: refs/tags/*
docker-dev:
event:
- tag
- name: docker-release-latest
image: plugins/docker
repo: vmaple/rpdata
tags: trunk
secrets: [ docker_username, docker_password ]
settings:
auto_tag: false
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-api
registry: r.vmaple.dev
when:
event: push
event:
- tag
+13 -2
View File
@@ -1,7 +1,18 @@
vendor
schema/bindata.go
graphql/schema/bindata.go
config.json
api
debug
build
.vscode
rpdata-graphiql
rpdata-*
!rpdata-*/
.vscode
.idea
/main
/main.exe
/graph2/graphcore/*_gen.go
generated.gql
generated.go
+12 -35
View File
@@ -1,50 +1,27 @@
## 1. Server
# Use Go 1.10
FROM golang:1.10 AS build-server
## 1. Build Server
# Use Go 1.14
FROM golang:1.14 AS build-server
# Load repository into docker
WORKDIR /go/src/git.aiterp.net/rpdata/api/
WORKDIR /project/
COPY . .
# Load build tools and dependencies
RUN go get -u github.com/golang/dep/cmd/dep
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN dep ensure
# Install build tools and dependencies
RUN go generate ./...
# Build server and tools (CGO disabled to make them 100% static)
ENV CGO_ENABLED 0
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-graphiql ./cmd/rpdata-graphiql
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2charimport ./cmd/rpdata-lb2charimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
## 2. React UI
# Use Node.JS 18
FROM node:10.5-alpine AS build-id
# The content is already there
WORKDIR /rpdata-ui
COPY ./rpdata-ui /rpdata-ui
# Prepare dependencies
RUN npm install
# Run build
RUN npm run build
## 3. Regroup
## 2. Distribute
# Use alpine linux
FROM alpine:3.7
FROM alpine:3.12
# Install certificates to enable HTTPS GET requests
RUN apk add --no-cache ca-certificates
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
RUN apk add --no-cache ca-certificates tzdata
# Copy build files into final container
COPY --from=build-server /binaries/* /usr/local/bin/
COPY --from=build-id /rpdata-ui/build /var/www/
# TODO: Use proper server instead of grapihql server
CMD ["/usr/local/bin/rpdata-graphiql"]
# The server is the main entry point
CMD ["/usr/local/bin/rpdata-server"]
Generated
-178
View File
@@ -1,178 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "git.aiterp.net/aiterp/wikiauth"
packages = ["."]
revision = "33860de804ddcec4fc17998745233b4ec477e7c2"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
branch = "master"
name = "github.com/globalsign/mgo"
packages = [
".",
"bson",
"internal/json",
"internal/sasl",
"internal/scram"
]
revision = "f76e4f9da92ecd56e3be26f5ba92580af1ef97b4"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "ace140f73450505f33e8b8418216792275ae82a7"
version = "v1.35.0"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "d523deb1b23d913de5bdada721a6071e71283618"
version = "v1.4.0"
[[projects]]
name = "github.com/graph-gophers/dataloader"
packages = ["."]
revision = "78139374585c29dcb97b8f33089ed11959e4be59"
version = "v5"
[[projects]]
branch = "master"
name = "github.com/graph-gophers/graphql-go"
packages = [
".",
"errors",
"internal/common",
"internal/exec",
"internal/exec/packer",
"internal/exec/resolvable",
"internal/exec/selected",
"internal/query",
"internal/schema",
"internal/validation",
"introspection",
"log",
"relay",
"trace"
]
revision = "9ebf33af539ab8cb832c7107bc0a978ca8dbc0de"
[[projects]]
branch = "master"
name = "github.com/jmoiron/sqlx"
packages = [
".",
"reflectx"
]
revision = "0dae4fefe7c0e190f7b5a78dac28a1c82cc8d849"
[[projects]]
name = "github.com/minio/minio-go"
packages = [
".",
"pkg/credentials",
"pkg/encrypt",
"pkg/s3signer",
"pkg/s3utils",
"pkg/set"
]
revision = "3d2d02921f0510e9d1f66ef77a265b8dddd36992"
version = "6.0.0"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log"
]
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
name = "github.com/sadbox/mediawiki"
packages = ["."]
revision = "39fea8a1336076a961a300d1d95765dcd17e8a3c"
version = "v0.1"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"argon2",
"blake2b",
"ssh/terminal"
]
revision = "d6449816ce06963d9d136eee5a56fca5b0616e7e"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"idna",
"lex/httplex"
]
revision = "a35a21de978d84ffc92f010a153705b170b2f9d1"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "2f57af4873d00d535c5c9028850aa2152e6a5566"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
name = "google.golang.org/appengine"
packages = ["cloudsql"]
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "16fd84401bc5796ffeaa895f31d099e1a23b363b15b87386877de0829ac9575b"
solver-name = "gps-cdcl"
solver-version = 1
-41
View File
@@ -1,41 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/globalsign/mgo"
[[constraint]]
name = "github.com/minio/minio-go"
version = "6.0.0"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/graph-gophers/graphql-go"
+2 -15
View File
@@ -1,18 +1,5 @@
# RPData API
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki, into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
## Progress
### Complete
* Session
* Character
* Log
* Post
* File
### Remaining
* Story
### Future
* User (manual DB edits will suffice for now)
There is some renovation going on, and the packages `model` and `graphql` are going to be superceded by `models` and `graph2`
-139
View File
@@ -1,139 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"strings"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/story"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var flagHost = flag.String("host", "127.0.0.1:3306", "SQL host")
var flagDB = flag.String("database", "aitestory", "SQL database")
var flagUser = flag.String("user", "aitestory", "SQL user")
var flagPassword = flag.String("password", "", "SQL password")
func main() {
flag.Parse()
db, err := sqlx.Connect("mysql", fmt.Sprintf("%s:%s@(%s)/%s", *flagUser, *flagPassword, *flagHost, *flagDB))
if err != nil {
log.Fatalln(err)
}
err = db.Ping()
if err != nil {
log.Fatalln(err)
}
store.Init()
results := make([]storyResult, 0, 64)
rows, err := db.Queryx("SELECT * FROM page WHERE unlisted=0;")
for rows.Next() {
result := storyResult{}
err := rows.StructScan(&result)
if err != nil {
log.Fatalln(err)
}
results = append(results, result)
}
tagResults := make([]tagResult, 0, 256)
rows, err = db.Queryx("SELECT page_id,type,name FROM page_tag LEFT JOIN tag ON tag_id=tag.id;")
for rows.Next() {
result := tagResult{}
err := rows.StructScan(&result)
if err != nil {
log.Fatalln(err)
}
tagResults = append(tagResults, result)
}
for _, result := range results {
fictionalDate, err := time.Parse("2006-01-02 15:04:05", result.FictionalDate)
if err != nil {
if result.FictionalDate != "0000-00-00 00:00:00" {
log.Fatalln(err)
}
}
if fictionalDate.Year() < 1800 {
fictionalDate = time.Time{}
}
publishDate, err := time.Parse("2006-01-02 15:04:05", result.PublishDate)
if err != nil {
log.Fatalln(err)
}
tags := make([]story.Tag, 0, 8)
for _, tagResult := range tagResults {
if tagResult.PageID == result.ID {
tags = append(tags, story.Tag{Kind: tagResult.Type, Name: tagResult.Name})
}
}
story, err := story.New(result.Name, result.Author[5:], result.Category, false, false, tags, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
// Change the story title
title := result.Name
if strings.HasPrefix(result.Source, "#") {
firstNewline := strings.Index(result.Source, "\n")
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
result.Source = result.Source[firstNewline+1:]
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
result.Source = result.Source[1:]
}
}
chapter, err := story.AddChapter(title, result.Author[5:], result.Source, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
if !result.Unlisted {
listed := true
err := story.Edit(nil, nil, &listed, nil, nil)
if err != nil {
log.Fatalln(err)
}
}
fmt.Println(result.ID, "->", story.ID, chapter.ID)
}
}
type tagResult struct {
PageID string `db:"page_id"`
Type string `db:"type"`
Name string `db:"name"`
}
type storyResult struct {
ID string `db:"id"`
Name string `db:"name"`
Author string `db:"author"`
Category string `db:"category"`
FictionalDate string `db:"fictional_date"`
PublishDate string `db:"publish_date"`
EditDate string `db:"edit_date"`
Unlisted bool `db:"unlisted"`
Dated bool `db:"dated"`
Spesific bool `db:"specific"`
Indexed bool `db:"indexed"`
Published bool `db:"published"`
Type string `db:"type"`
Source string `db:"source"`
Cache string `db:"cache"`
BackgroundURL *string `db:"background_url"`
}
-42
View File
@@ -1,42 +0,0 @@
package main
import (
"fmt"
"os"
"git.aiterp.net/rpdata/api/model/channel"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/log"
)
func main() {
err := store.Init()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
logs, err := log.List(32768)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
added := make(map[string]bool, 1024)
for _, log := range logs {
if added[log.Channel] {
continue
}
_, err := channel.Ensure(log.Channel, false)
if err != nil {
fmt.Fprintln(os.Stderr, log.ID, err)
continue
}
added[log.Channel] = true
fmt.Println(log.Channel, "ensured")
}
}
-2
View File
@@ -1,2 +0,0 @@
config.json
debug
-150
View File
@@ -1,150 +0,0 @@
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/loader"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/file"
logModel "git.aiterp.net/rpdata/api/model/log"
"git.aiterp.net/rpdata/api/resolver"
"git.aiterp.net/rpdata/api/schema"
graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
)
func main() {
err := store.Init()
if err != nil {
log.Fatalln("Failed to init store:", err)
}
n, err := logModel.UpdateAllCharacters()
if err != nil {
log.Println("Charcter updated stopped:", err)
}
log.Println("Updated characters on", n, "logs")
schema, err := graphql.ParseSchema(schema.String(), &resolver.RootResolver{}, graphql.MaxParallelism(48))
if err != nil {
log.Fatalln("Failed to parse schema:", err)
}
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Write(page)
}))
relayHandler := &relay.Handler{Schema: schema}
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
r = session.Load(w, r)
l := loader.New()
r = r.WithContext(l.ToContext(r.Context()))
relayHandler.ServeHTTP(w, r)
})
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
r = session.Load(w, r)
sess := session.FromContext(r.Context())
user := sess.User()
if user == nil || !user.Permitted("file.upload") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if err := r.ParseMultipartForm(16384); err != nil {
http.Error(w, "Internal (1): "+err.Error(), http.StatusInternalServerError)
return
}
formFile, header, err := r.FormFile("file")
if err != nil || header == nil || formFile == nil {
http.Error(w, "No file provided", http.StatusBadRequest)
return
}
file, err := file.Upload(r.Context(), header.Filename, header.Header.Get("Content-Type"), user.ID, header.Size, formFile)
if err != nil {
http.Error(w, "Internal (2): "+err.Error(), http.StatusInternalServerError)
return
}
json, err := json.Marshal(file)
if err != nil {
http.Error(w, "Internal (3): "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(json)
})
change.InitializeTimeline()
go func() {
index := int64(0)
for {
index++
ch, err := change.Timeline.Get(context.Background(), index)
if err == change.ErrExpired {
continue
} else if err != nil {
log.Println("Timeline:", err)
break
}
log.Printf("Change: time=%s model=%s op=%s id=%s author=%s\n", ch.Time.Local().Format("[Mon Jan 02 15:04 MST]"), ch.Model, ch.Op, ch.ObjectID, ch.Author)
}
}()
log.Fatal(http.ListenAndServe(":17000", nil))
}
var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/graphql", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
-3
View File
@@ -1,3 +0,0 @@
characters.json
characters.cson
debug
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"encoding/json"
"io"
"strings"
)
type charInfo struct {
Nicks []string `json:"nicks"`
Name string `json:"name"`
Author string `json:"player"`
ShortName string `json:"first"`
}
func load(reader io.Reader) ([]charInfo, error) {
data := make(map[string]interface{})
err := json.NewDecoder(reader).Decode(&data)
if err != nil {
return nil, err
}
links := make(map[string]string, len(data))
infos := make([]charInfo, 0, 64)
for key, value := range data {
if info, ok := value.(map[string]interface{}); ok {
name := info["name"].(string)
author := info["player"].(string)
shortName, ok := info["first"].(string)
if !ok {
shortName = strings.SplitN(name, " ", 2)[0]
}
infos = append(infos, charInfo{
Nicks: []string{key},
Name: name,
Author: author,
ShortName: shortName,
})
} else if nick, ok := value.(string); ok {
links[key] = nick
}
}
for key, value := range links {
for i := range infos {
if infos[i].Nicks[0] == value {
infos[i].Nicks = append(infos[i].Nicks, key)
}
}
}
return infos, nil
}
-62
View File
@@ -1,62 +0,0 @@
package main
import (
"flag"
"log"
"os"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/character"
)
var fileName = flag.String("file", "./characters.json", "json file to load")
func main() {
file, err := os.Open(*fileName)
if err != nil {
log.Fatalln("Open:", err)
}
infos, err := load(file)
if err != nil {
log.Fatalln("Parse:", err)
}
err = store.Init()
if err != nil {
log.Fatalln("Store:", err)
}
charsAdded, charsFailed := 0, 0
nicksAdded, nicksFailed := 0, 0
for _, info := range infos {
char, err := character.New(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
if err != nil {
log.Println(info.Nicks[0], "failed to insert:", err)
charsFailed++
char, err = character.FindNick(info.Nicks[0])
if err != nil {
continue
}
} else {
log.Println(info.Nicks[0], "added")
charsAdded++
}
for _, alt := range info.Nicks[1:] {
err := char.AddNick(alt)
if err != nil {
log.Println(info.Nicks[0], "failed to add nick", alt, "error:", err)
nicksFailed++
} else {
log.Println(info.Nicks[0], "addded nick", alt)
nicksAdded++
}
}
}
log.Printf("Characters – %d/%d", charsAdded, charsFailed+charsAdded)
log.Printf("Alt. Nicks – %d/%d", nicksAdded, nicksFailed+nicksAdded)
}
-2
View File
@@ -1,2 +0,0 @@
logs
debug
-85
View File
@@ -1,85 +0,0 @@
package main
import (
"bufio"
"errors"
"io"
"strconv"
"strings"
"time"
)
type logLine struct {
Verb string
Args []string
Text string
}
func parseFile(reader io.Reader) []logLine {
bufReader := bufio.NewReader(reader)
results := make([]logLine, 0, 512)
for {
line, err := bufReader.ReadString('\n')
if err == io.EOF {
if len(line) < 2 {
break
}
} else if err != nil {
break
}
if len(line) <= 2 {
continue
}
line = strings.Replace(line, "\n", "", 1)
line = strings.Replace(line, "\r", "", 1)
results = append(results, parseLine(line))
}
return results
}
func parseLine(line string) logLine {
textSplit := strings.SplitN(line, " :", 2)
tokens := strings.Split(textSplit[0], " ")
ll := logLine{
Verb: tokens[0],
}
if len(tokens) > 1 {
ll.Args = tokens[1:]
}
if len(textSplit) > 1 {
ll.Text = textSplit[1]
}
return ll
}
func parseFilename(fname string) (date time.Time, channel string, err error) {
date, err = time.ParseInLocation("2006-01-02_150405", fname[:17], time.Local)
if err != nil {
return
}
ms, err := strconv.Atoi(fname[17:20])
if err != nil {
return
}
date = date.Add(time.Duration(ms) * time.Millisecond)
if len(fname) < 23 {
err = errors.New("filename too short")
return
}
channel = fname[21:]
return
}
-108
View File
@@ -1,108 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path"
"strings"
"time"
"git.aiterp.net/rpdata/api/internal/store"
logModel "git.aiterp.net/rpdata/api/model/log"
)
var prefixReplacer = strings.NewReplacer("+", "", "@", "", "!", "", "%", "")
func main() {
err := store.Init()
if err != nil {
log.Fatalln(err)
}
for _, filepath := range os.Args[1:] {
name := strings.Replace(path.Base(filepath), ".txt", "", 1)
file, err := os.Open(filepath)
if err != nil {
log.Println(filepath, err)
continue
}
logLines := parseFile(file)
file.Close()
// Get title and event
title := ""
event := ""
for _, line := range logLines {
if line.Verb == "TITLE" {
title = line.Text
}
if line.Verb == "TAG" {
event = line.Text
}
}
date, channel, err := parseFilename(name)
if err != nil {
log.Fatalln(err)
}
l, err := logModel.New(date, channel, title, event, "", false)
if err != nil {
log.Println(err)
continue
}
_, err = l.NewPost(time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
if err != nil {
log.Println(err)
}
for _, line := range logLines {
if line.Verb != "CHARS" {
continue
}
_, err = l.NewPost(l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
if err != nil {
log.Println(err)
}
}
for _, line := range logLines {
if line.Verb != "SCENE" && line.Verb != "ACTION" && line.Verb != "TEXT" {
continue
}
postTime, err := time.ParseInLocation("2006-01-02 15:04:05", date.Format("2006-01-02")+" "+line.Args[1], time.Local)
diff := postTime.Sub(date)
if err != nil {
log.Println(err)
continue
}
if diff < 0 {
if diff > -time.Second {
postTime = postTime.Add(diff)
} else {
postTime = postTime.Add(time.Hour * 24)
}
}
if line.Args[0][0] == '=' {
line.Verb = "SCENE"
}
_, err = l.NewPost(postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
if err != nil {
log.Println(err)
}
}
err = l.UpdateCharacters()
if err != nil {
log.Println(err)
}
fmt.Println(l.ID, "completed")
}
}
+126
View File
@@ -0,0 +1,126 @@
package main
import (
"context"
"fmt"
"git.aiterp.net/rpdata/api/space"
"log"
"net/http"
"runtime/debug"
"strings"
"git.aiterp.net/rpdata/api/database"
"git.aiterp.net/rpdata/api/graph2"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/instrumentation"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/services"
"github.com/99designs/gqlgen/handler"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
cfg := config.Global()
db, err := database.Init(cfg.Database)
if err != nil {
log.Fatalln("Failed to init db:", err)
}
defer db.Close(context.Background())
var spaceClient *space.Client
if cfg.Space.Enabled {
spaceClient, err = space.Connect(cfg.Space)
if err != nil {
log.Fatalln("Failed to init space:", err)
}
log.Println("Space loaded")
} else {
log.Println("Space is disabled, file upload will not work!")
}
serviceBundle := services.NewBundle(db, spaceClient)
instrumentation.Register()
http.Handle("/", handler.Playground("RPData API", "/graphql"))
http.Handle("/graphql", queryHandler(serviceBundle))
http.Handle("/metrics", promhttp.Handler())
go func() {
err := serviceBundle.Logs.RefreshAllLogCharacters(context.Background())
if err != nil {
log.Println("Refresh Characters:", err)
}
err = serviceBundle.Logs.FixImportDateBug(context.Background())
if err != nil {
log.Println("Fix date bug:", err)
}
}()
go logListedChanges(serviceBundle.Changes)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func logListedChanges(changes *services.ChangeService) {
sub := changes.Subscribe(context.Background(), models.ChangeFilter{})
for change := range sub {
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)
for _, object := range change.Objects() {
switch object := object.(type) {
case models.Log:
log.Printf("\tLog: %s (%s)", object.ID, object.ShortID)
case models.Character:
log.Printf("\tCharacter: %s", object.ID)
case models.Channel:
log.Printf("\tChannel: %s", object.Name)
case models.Post:
log.Printf("\tPost: %s", object.ID)
case models.Story:
log.Printf("\tStory: %s", object.ID)
case models.Tag:
log.Printf("\tTag: (%s,%s)", object.Kind, object.Name)
case models.Chapter:
log.Printf("\tChapter: %s", object.ID)
case models.Comment:
log.Printf("\tComment: %s", object.ID)
}
}
}
log.Println("Change subscription closed.")
}
func queryHandler(services *services.Bundle) http.HandlerFunc {
handler := handler.GraphQL(
graph2.New(services),
handler.UploadMaxSize(10485760),
handler.UploadMaxMemory(1048576),
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
log.Println(err)
log.Println(string(debug.Stack()))
return fmt.Errorf("The server failed to serve your request due to an internal error, it has been logged")
}),
handler.ComplexityLimit(300),
handler.RequestMiddleware(instrumentation.RequestMiddleware()),
handler.ResolverMiddleware(instrumentation.ResolverMiddleware()),
)
return func(w http.ResponseWriter, r *http.Request) {
// >_>
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer of the curse") {
w.Header().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
w.Header().Add("X-Emerald-Herald", "Seek the king, that is the only way.")
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
}
r = services.Auth.RequestWithToken(r)
handler.ServeHTTP(w, r)
}
}
-77
View File
@@ -1,77 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/file"
)
func main() {
client := http.Client{Timeout: time.Second * 30}
res, err := client.Get(config.Global().Wiki.URL + "?action=query&list=allimages&ailimit=5&aiprop=dimensions|mime|url|user|timestamp&format=json&ailimit=500")
if err != nil {
log.Fatalln(err)
}
if res.StatusCode != 200 {
log.Fatalln(res.Status)
}
data := ResponseData{}
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
log.Fatalln(err)
}
store.Init()
for _, info := range data.Query.Allimages {
existing, err := file.FindName("wiki", info.Name, info.User)
if err == nil && existing.ID != "" {
log.Println("Skipping", info.Name, "because it already exists")
continue
}
ts, err := time.Parse(time.RFC3339, info.Timestamp)
if err != nil {
log.Println("Skipping", info.Name, "error:", err)
continue
}
file, err := file.Insert(info.Name, "wiki", info.Mime, info.User, ts, info.Size, info.URL)
if err != nil {
log.Println("Skipping", info.Name, "error:", err)
continue
}
fmt.Println(file.Name, "inserted ( id:", file.ID, ")")
}
}
// The ResponseData from the wiki looks like this
type ResponseData struct {
Batchcomplete string `json:"batchcomplete"`
Query struct {
Allimages []struct {
Descriptionshorturl string `json:"descriptionshorturl"`
Descriptionurl string `json:"descriptionurl"`
Height int `json:"height"`
Mime string `json:"mime"`
Name string `json:"name"`
Ns int `json:"ns"`
Size int64 `json:"size"`
Timestamp string `json:"timestamp"`
Title string `json:"title"`
URL string `json:"url"`
User string `json:"user"`
Width int `json:"width"`
} `json:"allimages"`
} `json:"query"`
}
+40
View File
@@ -0,0 +1,40 @@
package database
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/database/mongodb"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/repositories"
)
// ErrDriverUnrecognized is returned if the driver is not recognized
var ErrDriverUnrecognized = errors.New("driver not recognized, check installed version or your configuration")
type Database interface {
Changes() repositories.ChangeRepository
Channels() repositories.ChannelRepository
Characters() repositories.CharacterRepository
Tags() repositories.TagRepository
Logs() repositories.LogRepository
Posts() repositories.PostRepository
Stories() repositories.StoryRepository
Chapters() repositories.ChapterRepository
Comments() repositories.CommentRepository
Keys() repositories.KeyRepository
Users() repositories.UserRepository
Files() repositories.FileRepository
Close(ctx context.Context) error
}
// Init sets up the database.
func Init(config config.Database) (Database, error) {
switch config.Driver {
case "mongo", "mgo", "mongodb":
return mongodb.Init(config)
default:
return nil, ErrDriverUnrecognized
}
}
+109
View File
@@ -0,0 +1,109 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"strconv"
"time"
)
type changeRepository struct {
changes *mgo.Collection
idCounter *counter
}
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
change := new(models.Change)
err := r.changes.FindId(id).One(change)
if err != nil {
return nil, err
}
return change, nil
}
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
query := bson.M{}
limit := 0
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() {
query["date"] = bson.M{"$gte": *filter.EarliestDate}
}
if len(filter.Keys) > 0 {
query["keys"] = bson.M{"$in": filter.Keys}
} else {
query["listed"] = true
}
if filter.Author != nil && *filter.Author != "" {
query["author"] = *filter.Author
}
if filter.Limit != nil {
limit = *filter.Limit
}
initialSize := 64
if limit > 0 && limit < 256 {
initialSize = limit
}
changes := make([]*models.Change, 0, initialSize)
err := r.changes.Find(query).Sort("-date").Limit(limit).All(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
next, err := r.idCounter.Increment(1)
if err != nil {
return nil, err
}
change.ID = "Change_" + strconv.Itoa(next)
err = r.changes.Insert(&change)
if err != nil {
return nil, err
}
return &change, nil
}
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
return r.changes.RemoveId(change.ID)
}
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) {
collection := db.C("common.changes")
// Delete the old index if it exists.
_ = collection.DropIndexName("date_1")
err := collection.EnsureIndex(mgo.Index{
Name: "expiry",
Key: []string{"date"},
ExpireAfter: time.Hour * 2400, // 100 days
})
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("keys")
if err != nil {
return nil, err
}
return &changeRepository{
changes: collection,
idCounter: newCounter(db, "auto_increment", "Change"),
}, nil
}
+107
View File
@@ -0,0 +1,107 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
func newChannelRepository(db *mgo.Database) (*channelRepository, error) {
collection := db.C("common.channels")
err := collection.EnsureIndexKey("logged")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("event")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("location")
if err != nil {
return nil, err
}
return &channelRepository{
channels: collection,
}, nil
}
type channelRepository struct {
channels *mgo.Collection
}
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
channel := new(models.Channel)
err := r.channels.FindId(name).One(channel)
if err != nil {
return nil, err
}
return channel, nil
}
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
query := bson.M{}
if filter.Logged != nil {
query["logged"] = *filter.Logged
}
if filter.EventName != nil {
query["eventName"] = *filter.EventName
}
if filter.LocationName != nil {
query["locationName"] = *filter.LocationName
}
if len(filter.Names) > 0 {
query["_id"] = bson.M{"$in": filter.Names}
}
channels := make([]*models.Channel, 0, 32)
err := r.channels.Find(query).Limit(filter.Limit).Sort("_id").All(&channels)
if err != nil {
if err == mgo.ErrNotFound {
return channels, nil
}
return nil, err
}
return channels, nil
}
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
err := r.channels.Insert(channel)
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *channelRepository) Update(ctx context.Context, channel models.Channel, update models.ChannelUpdate) (*models.Channel, error) {
updateBson := bson.M{}
if update.Logged != nil {
updateBson["logged"] = *update.Logged
channel.Logged = *update.Logged
}
if update.LocationName != nil {
updateBson["locationName"] = *update.LocationName
channel.LocationName = *update.LocationName
}
if update.EventName != nil {
updateBson["eventName"] = *update.EventName
channel.EventName = *update.EventName
}
err := r.channels.UpdateId(channel.Name, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &channel, nil
}
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
return r.channels.RemoveId(channel.Name)
}
+137
View File
@@ -0,0 +1,137 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
)
type chapterRepository struct {
chapters *mgo.Collection
comments *mgo.Collection
}
func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) {
collection := db.C("story.chapters")
err := collection.EnsureIndexKey("storyId")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("createdDate")
if err != nil {
return nil, err
}
return &chapterRepository{
chapters: collection,
comments: db.C("story.comments"),
}, nil
}
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
chapter := new(models.Chapter)
err := r.chapters.FindId(id).One(chapter)
if err != nil {
return nil, err
}
return chapter, nil
}
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
query := bson.M{}
if filter.StoryID != nil {
query["storyId"] = *filter.StoryID
}
chapters := make([]*models.Chapter, 0, 32)
err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters)
if err != nil {
if err == mgo.ErrNotFound {
return chapters, nil
}
return nil, err
}
return chapters, nil
}
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
chapter.ID = generate.StoryID()
err := r.chapters.Insert(chapter)
if err != nil {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
updateBson := bson.M{}
if update.Title != nil {
updateBson["title"] = *update.Title
chapter.Title = *update.Title
}
if update.Source != nil {
updateBson["source"] = *update.Source
chapter.Source = *update.Source
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
chapter.FictionalDate = *update.FictionalDate
}
if update.CommentMode != nil {
updateBson["commentMode"] = *update.CommentMode
chapter.CommentMode = *update.CommentMode
}
if update.CommentsLocked != nil {
updateBson["commentsLocked"] = *update.CommentsLocked
chapter.CommentsLocked = *update.CommentsLocked
}
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &chapter, nil
}
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) {
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.ID}})
if err != nil {
return nil, err
}
chapter.StoryID = to.ID
return &chapter, nil
}
func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error {
err := r.chapters.RemoveId(chapter.ID)
if err != nil {
return err
}
c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID})
if err != nil {
log.Println("Failed to remove comments:", err)
return nil
}
log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed)
return nil
}
+248
View File
@@ -0,0 +1,248 @@
package mongodb
import (
"context"
"errors"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"sort"
"strconv"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
)
type characterRepository struct {
characters *mgo.Collection
cidCounter *counter
}
func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository, error) {
collection := db.C("common.characters")
err := collection.EnsureIndexKey("name")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("shortName")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
return nil, err
}
err = collection.EnsureIndex(mgo.Index{
Key: []string{"$text:description"},
})
if err != nil {
return nil, err
}
return &characterRepository{
characters: collection,
cidCounter: newCounter(db, "auto_increment", "Character"),
}, nil
}
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
character := new(models.Character)
err := r.characters.FindId(id).One(character)
if err != nil {
return nil, err
}
return character, nil
}
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
character := new(models.Character)
err := r.characters.Find(bson.M{"nick": nick}).One(character)
if err != nil {
return nil, err
}
return character, nil
}
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
query := bson.M{
"$or": []bson.M{
{"shortName": name},
{"name": name},
},
}
// Look for all characters matching query
characters := make([]*models.Character, 0, 8)
err := r.characters.Find(query).All(&characters)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
} else if len(characters) == 0 {
return nil, repositories.ErrNotFound
}
// Prioritize exact match
for _, character := range characters {
if character.Name == name {
return character, nil
}
}
return characters[0], nil
}
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if len(filter.IDs) > 0 {
query["_id"] = bson.M{"$in": filter.IDs}
}
if len(filter.Nicks) > 0 {
query["nicks"] = bson.M{"$in": filter.Nicks}
}
if len(filter.Names) > 0 {
query["$or"] = []bson.M{
{"name": bson.M{"$in": filter.Names}},
{"shortName": bson.M{"$in": filter.Names}},
}
}
if filter.Search != nil {
query["$text"] = bson.M{"$search": *filter.Search}
}
characters := make([]*models.Character, 0, 32)
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
if err != nil {
if err == mgo.ErrNotFound {
return characters, nil
}
return nil, err
}
sort.Slice(characters, func(i, j int) bool {
ni, _ := strconv.Atoi(characters[i].ID[1:])
nj, _ := strconv.Atoi(characters[j].ID[1:])
return ni < nj
})
return characters, nil
}
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
nextId, err := r.cidCounter.Increment(1)
if err != nil {
return nil, err
}
character.ID = "C" + strconv.Itoa(nextId)
err = r.characters.Insert(&character)
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) {
updateBson := bson.M{}
if update.Name != nil {
updateBson["name"] = *update.Name
character.Name = *update.Name
}
if update.ShortName != nil {
updateBson["shortName"] = *update.ShortName
character.ShortName = *update.ShortName
}
if update.Description != nil {
updateBson["description"] = *update.Description
character.Description = *update.Description
}
err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &character, nil
}
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
if character.HasNick(nick) {
return nil, errors.New("nick already exist")
}
match := bson.M{
"_id": character.ID,
"nicks": bson.M{"$ne": nick},
}
err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}})
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
} else if err != nil {
return nil, err
}
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
copy(newNicks, character.Nicks)
character.Nicks = append(newNicks, nick)
return &character, nil
}
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
if !character.HasNick(nick) {
return nil, errors.New("nick does not exist")
}
match := bson.M{
"_id": character.ID,
"nicks": nick,
}
err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}})
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
} else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 {
return nil, errors.New("The nick belongs to another character already")
} else if err != nil {
return nil, err
}
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
copy(newNicks, character.Nicks)
for i := range newNicks {
if newNicks[i] == nick {
newNicks = append(newNicks[:i], newNicks[i+1:]...)
break
}
}
character.Nicks = newNicks
return &character, nil
}
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
return r.characters.RemoveId(character.ID)
}
+134
View File
@@ -0,0 +1,134 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
)
type commentRepository struct {
comments *mgo.Collection
}
func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) {
collection := db.C("story.comments")
err := collection.EnsureIndexKey("chapterId")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("createdDate")
if err != nil {
return nil, err
}
r := &commentRepository{
comments: collection,
}
go r.fixFieldTypo()
return r, nil
}
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
comment := new(models.Comment)
err := r.comments.FindId(id).One(comment)
if err != nil {
return nil, err
}
return comment, nil
}
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
query := bson.M{}
if filter.ChapterID != nil {
query["chapterId"] = *filter.ChapterID
}
comments := make([]*models.Comment, 0, 32)
err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments)
if err != nil {
if err == mgo.ErrNotFound {
return comments, nil
}
return nil, err
}
return comments, nil
}
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
comment.ID = generate.CommentID()
err := r.comments.Insert(comment)
if err != nil {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
updateBson := bson.M{}
if update.Subject != nil {
updateBson["subject"] = *update.Subject
comment.Subject = *update.Subject
}
if update.Source != nil {
updateBson["source"] = *update.Source
comment.Source = *update.Source
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
comment.FictionalDate = *update.FictionalDate
}
if update.CharacterID != nil {
updateBson["characterId"] = *update.CharacterID
comment.CharacterID = *update.CharacterID
}
if update.CharacterName != nil {
updateBson["characterName"] = *update.CharacterName
comment.CharacterName = *update.CharacterName
}
err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &comment, nil
}
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
return r.comments.RemoveId(comment.ID)
}
func (r *commentRepository) fixFieldTypo() {
c, err := r.comments.UpdateAll(bson.M{
"editeddDate": bson.M{"$ne": nil},
}, bson.M{
"$rename": bson.M{"editeddDate": "editedDate"},
})
if err != nil {
if err == mgo.ErrNotFound {
return
}
log.Println("Failed to run name typo fix:", err)
return
}
if c.Updated > 0 {
log.Println("Fixed editeddDate field name typo in", c.Updated, "comments")
}
}
+249
View File
@@ -0,0 +1,249 @@
package mongodb
import (
"context"
"fmt"
"github.com/globalsign/mgo/bson"
"time"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
)
type MongoDB struct {
session *mgo.Session
changes repositories.ChangeRepository
channels *channelRepository
characters repositories.CharacterRepository
tags repositories.TagRepository
logs *logRepository
posts *postRepository
files *fileRepository
stories repositories.StoryRepository
chapters repositories.ChapterRepository
comments repositories.CommentRepository
keys repositories.KeyRepository
users repositories.UserRepository
}
func (m *MongoDB) Changes() repositories.ChangeRepository {
return m.changes
}
func (m *MongoDB) Characters() repositories.CharacterRepository {
return m.characters
}
func (m *MongoDB) Tags() repositories.TagRepository {
return m.tags
}
func (m *MongoDB) Logs() repositories.LogRepository {
return m.logs
}
func (m *MongoDB) Posts() repositories.PostRepository {
return m.posts
}
func (m *MongoDB) Channels() repositories.ChannelRepository {
return m.channels
}
func (m *MongoDB) Files() repositories.FileRepository {
return m.files
}
func (m *MongoDB) Stories() repositories.StoryRepository {
return m.stories
}
func (m *MongoDB) Chapters() repositories.ChapterRepository {
return m.chapters
}
func (m *MongoDB) Comments() repositories.CommentRepository {
return m.comments
}
func (m *MongoDB) Keys() repositories.KeyRepository {
return m.keys
}
func (m *MongoDB) Users() repositories.UserRepository {
return m.users
}
func (m *MongoDB) Close(ctx context.Context) error {
m.session.Close()
return nil
}
// Init initializes the mongodb database
func Init(cfg config.Database) (*MongoDB, error) {
port := cfg.Port
if port <= 0 {
port = 27017
}
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{fmt.Sprintf("%s:%d", cfg.Host, port)},
Timeout: 30 * time.Second,
Database: cfg.Db,
Username: cfg.Username,
Password: cfg.Password,
Mechanism: cfg.Mechanism,
Source: cfg.Db,
})
if err != nil {
return nil, err
}
db := session.DB(cfg.Db)
characters, err := newCharacterRepository(db)
if err != nil {
session.Close()
return nil, err
}
channels, err := newChannelRepository(db)
if err != nil {
session.Close()
return nil, err
}
changes, err := newChangeRepository(db)
if err != nil {
session.Close()
return nil, err
}
logs, err := newLogRepository(db)
if err != nil {
session.Close()
return nil, err
}
posts, err := newPostRepository(db)
if err != nil {
session.Close()
return nil, err
}
files, err := newFileRepository(db)
if err != nil {
session.Close()
return nil, err
}
stories, err := newStoryRepository(db)
if err != nil {
session.Close()
return nil, err
}
chapters, err := newChapterRepository(db)
if err != nil {
session.Close()
return nil, err
}
comments, err := newCommentRepository(db)
if err != nil {
session.Close()
return nil, err
}
keys, err := newKeyRepository(db)
if err != nil {
session.Close()
return nil, err
}
users, err := newUserRepository(db)
if err != nil {
session.Close()
return nil, err
}
go posts.fixPositions(logs)
return &MongoDB{
session: session,
changes: changes,
characters: characters,
channels: channels,
tags: newTagRepository(db),
stories: stories,
chapters: chapters,
comments: comments,
logs: logs,
posts: posts,
files: files,
keys: keys,
users: users,
}, nil
}
type counter struct {
coll *mgo.Collection
category string
name string
}
func newCounter(db *mgo.Database, category, name string) *counter {
return &counter{
coll: db.C("core.counters"),
category: category,
name: name,
}
}
func (c *counter) WithName(name string) *counter {
return &counter{
coll: c.coll,
category: c.category,
name: name,
}
}
func (c *counter) WithCategory(category string) *counter {
return &counter{
coll: c.coll,
category: category,
name: c.name,
}
}
func (c *counter) With(category, name string) *counter {
return &counter{
coll: c.coll,
category: category,
name: name,
}
}
func (c *counter) Increment(amount int) (int, error) {
type counterDoc struct {
ID string `bson:"_id"`
Value int `bson:"value"`
}
id := c.category + "." + c.name
doc := counterDoc{}
_, err := c.coll.Find(bson.M{"_id": id}).Apply(mgo.Change{
Update: bson.M{"$inc": bson.M{"value": amount}},
Upsert: true,
ReturnNew: true,
}, &doc)
if err != nil {
return -1, err
}
return doc.Value, nil
}
+105
View File
@@ -0,0 +1,105 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type fileRepository struct {
files *mgo.Collection
}
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) {
file := new(models.File)
err := r.files.FindId(id).One(file)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
}
return file, nil
}
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if filter.MimeTypes != nil {
query["mimeType"] = bson.M{"$in": filter.MimeTypes}
}
if filter.Public != nil {
query["public"] = *filter.Public
}
files := make([]*models.File, 0, 16)
err := r.files.Find(query).Sort("-date").All(&files)
if err != nil {
return nil, err
}
return files, nil
}
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) {
file.ID = generate.FileID()
err := r.files.Insert(&file)
if err != nil {
return nil, err
}
return &file, nil
}
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) {
updateBson := bson.M{}
if update.Public != nil {
updateBson["public"] = *update.Public
file.Public = *update.Public
}
if update.Name != nil {
updateBson["name"] = *update.Name
file.Name = *update.Name
}
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &file, nil
}
func (r *fileRepository) Delete(ctx context.Context, file models.File) error {
return r.files.RemoveId(file.ID)
}
func newFileRepository(db *mgo.Database) (*fileRepository, error) {
files := db.C("file.headers")
err := files.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = files.EnsureIndexKey("mimeType")
if err != nil {
return nil, err
}
err = files.EnsureIndexKey("public")
if err != nil {
return nil, err
}
return &fileRepository{files: files}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type keyRepository struct {
keys *mgo.Collection
}
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
collection := db.C("auth.keys")
err := collection.EnsureIndexKey("user")
if err != nil {
return nil, err
}
return &keyRepository{keys: collection}, nil
}
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
key := models.Key{}
err := r.keys.FindId(id).One(&key)
if err != nil {
return nil, err
}
return &key, err
}
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
query := bson.M{}
if filter.UserID != nil {
query["user"] = *filter.UserID
}
keys := make([]*models.Key, 0, 4)
err := r.keys.Find(query).All(&keys)
if err != nil {
if err == mgo.ErrNotFound {
return nil, nil
}
return nil, err
}
return keys, nil
}
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
key.ID = generate.KeyID()
err := r.keys.Insert(&key)
if err != nil {
return nil, err
}
return &key, nil
}
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
return r.keys.RemoveId(key.ID)
}
+204
View File
@@ -0,0 +1,204 @@
package mongodb
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"strconv"
"sync"
)
type logRepository struct {
openMutex sync.Mutex
logs *mgo.Collection
posts *mgo.Collection
shortIdCounter *counter
}
func newLogRepository(db *mgo.Database) (*logRepository, error) {
logs := db.C("logbot3.logs")
posts := db.C("logbot3.posts")
err := logs.EnsureIndexKey("date")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("channel")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("characterIds")
if err != nil {
return nil, err
}
err = logs.EnsureIndexKey("event")
if err != nil {
return nil, err
}
err = logs.EnsureIndex(mgo.Index{Key: []string{"channel", "open"}})
if err != nil {
return nil, err
}
err = logs.EnsureIndex(mgo.Index{
Key: []string{"shortId"},
Unique: true,
DropDups: true,
})
if err != nil {
return nil, err
}
return &logRepository{
logs: logs,
posts: posts,
shortIdCounter: newCounter(db, "auto_increment", "Log"),
}, nil
}
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
log := new(models.Log)
err := r.logs.Find(bson.M{"$or": []bson.M{{"_id": id}, {"shortId": id}}}).One(log)
if err != nil {
return nil, err
}
return log, nil
}
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
query := bson.M{}
if filter.Search != nil {
searchQuery := bson.M{
"$text": bson.M{"$search": *filter.Search},
"logId": bson.M{"$ne": nil},
}
logIds := make([]string, 0, 64)
err := r.posts.Find(searchQuery).Distinct("logId", &logIds)
if err != nil {
return nil, err
}
query["shortId"] = bson.M{"$in": logIds}
}
if filter.Open != nil {
r.openMutex.Lock()
defer r.openMutex.Unlock()
query["open"] = filter.Open
}
if len(filter.Characters) > 0 {
query["characterIds"] = bson.M{"$all": filter.Characters}
}
if len(filter.Channels) > 0 {
query["channel"] = bson.M{"$in": filter.Channels}
}
if len(filter.Events) > 0 {
query["event"] = bson.M{"$in": filter.Events}
}
if filter.MinDate != nil && filter.MaxDate != nil {
query["date"] = bson.M{"$gte": *filter.MinDate, "$lte": *filter.MaxDate}
} else if filter.MinDate != nil {
query["date"] = bson.M{"$gte": *filter.MinDate}
} else if filter.MaxDate != nil {
query["date"] = bson.M{"$lt": *filter.MaxDate}
}
logs := make([]*models.Log, 0, 32)
err := r.logs.Find(query).Sort("-date").Limit(filter.Limit).All(&logs)
if err != nil {
if err == mgo.ErrNotFound {
return logs, nil
}
return nil, err
}
return logs, nil
}
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
nextShortId, err := r.shortIdCounter.Increment(1)
if err != nil {
return nil, err
}
log.ID = generate.LogID(log)
log.ShortID = "L" + strconv.Itoa(nextShortId)
if log.Open {
// There can be only one open log in the same channel.
r.openMutex.Lock()
defer r.openMutex.Unlock()
_, err = r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
if err != nil {
return nil, errors.New("Cannot close other logs: " + err.Error())
}
}
err = r.logs.Insert(&log)
if err != nil {
return nil, err
}
return &log, nil
}
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
updateBson := bson.M{}
if update.Open != nil {
if *update.Open == true {
// There can be only one open log in the same channel.
r.openMutex.Lock()
defer r.openMutex.Unlock()
_, err := r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
if err != nil {
return nil, errors.New("Cannot close other logs: " + err.Error())
}
}
updateBson["open"] = *update.Open
log.Open = *update.Open
}
if update.Title != nil {
updateBson["title"] = *update.Title
log.Title = *update.Title
}
if update.Description != nil {
updateBson["description"] = *update.Description
log.Description = *update.Description
}
if update.EventName != nil {
updateBson["event"] = *update.EventName
log.EventName = *update.EventName
}
if update.CharacterIDs != nil {
updateBson["characterIds"] = update.CharacterIDs
log.CharacterIDs = update.CharacterIDs
}
err := r.logs.UpdateId(log.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &log, nil
}
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
err := r.logs.RemoveId(log.ID)
if err != nil {
return err
}
_, _ = r.posts.RemoveAll(bson.M{"logId": log.ShortID})
return nil
}
+345
View File
@@ -0,0 +1,345 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
"strings"
"sync"
"time"
)
type postRepository struct {
logs *mgo.Collection
posts *mgo.Collection
orderMutex sync.Mutex
}
func newPostRepository(db *mgo.Database) (*postRepository, error) {
posts := db.C("logbot3.posts")
err := posts.EnsureIndexKey("logId")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("time")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("kind")
if err != nil {
return nil, err
}
err = posts.EnsureIndexKey("position")
if err != nil {
return nil, err
}
err = posts.EnsureIndex(mgo.Index{
Key: []string{"$text:text"},
})
if err != nil {
return nil, err
}
return &postRepository{
posts: posts,
logs: db.C("logbot3.logs"),
}, nil
}
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
post := new(models.Post)
err := r.posts.FindId(id).One(post)
if err != nil {
return nil, err
}
return post, nil
}
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
query := bson.M{}
if filter.LogID != nil && *filter.LogID != "" {
logId := *filter.LogID
if !strings.HasPrefix(logId, "L") {
// Resolve long id to short id
log := new(models.Log)
err := r.logs.FindId(logId).Select(bson.M{"logId": 1, "_id": 1}).One(log)
if err != nil {
return nil, err
}
logId = log.ShortID
}
query["logId"] = logId
}
if len(filter.IDs) > 0 {
query["_id"] = bson.M{"$in": filter.IDs}
}
if len(filter.Kinds) > 0 {
query["kind"] = bson.M{"$in": filter.Kinds}
}
if filter.Search != nil {
query["$text"] = bson.M{"$search": *filter.Search}
}
posts := make([]*models.Post, 0, 32)
err := r.posts.Find(query).Sort("-logId", "position").Limit(filter.Limit).All(&posts)
if err != nil {
if err == mgo.ErrNotFound {
return []*models.Post{}, nil
}
return nil, err
}
return posts, nil
}
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
lastPost := new(models.Post)
err := r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
post.ID = generate.PostID()
post.Position = lastPost.Position + 1 // Position 1 is first position, so this is safe.
err = r.posts.Insert(post)
if err != nil {
return nil, err
}
return &post, nil
}
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
if len(posts) == 0 {
return []*models.Post{}, nil
}
logId := posts[0].LogID
for _, post := range posts {
if post.LogID != logId {
return nil, repositories.ErrParentMismatch
}
post.ID = generate.PostID()
}
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
lastPost := new(models.Post)
err := r.posts.Find(bson.M{"logId": posts[0].LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
docs := make([]interface{}, len(posts))
for i := range posts {
posts[i].Position = lastPost.Position + 1 + i
docs[i] = posts[i]
}
err = r.posts.Insert(docs...)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
return posts, nil
}
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
updateBson := bson.M{}
if update.Time != nil {
updateBson["time"] = *update.Time
post.Time = *update.Time
}
if update.Kind != nil {
updateBson["kind"] = *update.Kind
post.Kind = *update.Kind
}
if update.Nick != nil {
updateBson["nick"] = *update.Nick
post.Nick = *update.Nick
}
if update.Text != nil {
updateBson["text"] = *update.Text
post.Text = *update.Text
}
err := r.posts.UpdateId(post.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &post, nil
}
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
// If only MongoDB transactions weren't awful, this function would have been safe.
// Since it isn't, then good luck.
// Validate lower position bound.
if position < 1 {
return nil, repositories.ErrInvalidPosition
}
// Determine the operations on adjacent posts
var resultFilter bson.M
var pushFilter bson.M
var increment int
if post.Position > position {
pushFilter = bson.M{"position": bson.M{
"$gte": position,
"$lt": post.Position,
}}
increment = 1
resultFilter = bson.M{"position": bson.M{
"$lte": post.Position,
"$gte": position,
}}
} else {
pushFilter = bson.M{"position": bson.M{
"$lte": position,
"$gt": post.Position,
}}
increment = -1
resultFilter = bson.M{"position": bson.M{
"$lte": position,
"$gte": post.Position,
}}
}
pushFilter["logId"] = post.LogID
resultFilter["logId"] = post.LogID
// From here on out, sync is required
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
// Detect ninja shenanigans
post2 := new(models.Post)
err := r.posts.FindId(post.ID).One(post2)
if err != nil && post2.Position != post.Position {
return nil, repositories.ErrNotFound
}
// Validate upper position bound
lastPost := new(models.Post)
err = r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
if position > lastPost.Position {
return nil, repositories.ErrInvalidPosition
}
// Move the posts
changeInfo, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": increment}})
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
err = r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": position}})
if err != nil {
// Try to undo it
_, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": -increment}})
if err != nil && err != mgo.ErrNotFound {
return nil, err
}
return nil, err
}
results := make([]*models.Post, 0, changeInfo.Matched+1)
err = r.posts.Find(resultFilter).All(&results)
if err != nil {
return nil, err
}
return results, nil
}
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
r.orderMutex.Lock()
defer r.orderMutex.Unlock()
err := r.posts.RemoveId(post.ID)
if err != nil {
return err
}
_, _ = r.posts.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
return nil
}
func (r *postRepository) fixPositions(logRepo *logRepository) {
disorders := make([]int, 0, 16)
diffs := make([]int, 0, 16)
startTime := time.Now()
timeout, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
logs, err := logRepo.List(timeout, models.LogFilter{})
if err != nil {
log.Println("Failed to get logs for position fix:", err)
}
log.Println("Starting log position fixing, this should not take longer than 10 seconds.")
for _, l := range logs {
r.orderMutex.Lock()
posts, err := r.List(timeout, models.PostFilter{LogID: &l.ShortID})
if err != nil {
r.orderMutex.Unlock()
continue
}
disorders = disorders[:0]
diffs = diffs[:0]
for i, post := range posts {
if post.Position != (i + 1) {
disorders = append(disorders, i)
diffs = append(diffs, post.Position-(i+1))
}
}
if len(disorders) > 0 {
log.Println(len(disorders), "order errors detected in", l.ID)
ops := 0
for i, post := range posts {
if (i + 1) != posts[i].Position {
ops++
err := r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": i + 1}})
if err != nil {
log.Println(l.ShortID, "fix failed after", ops, "ops:", err)
break
}
}
}
log.Println(l.ShortID, "fixed after", ops, "ops.")
}
r.orderMutex.Unlock()
}
log.Println("Log position fixing finished in", time.Since(startTime))
}
+214
View File
@@ -0,0 +1,214 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/internal/generate"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"log"
)
type storyRepository struct {
stories *mgo.Collection
chapters *mgo.Collection
comments *mgo.Collection
}
func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error) {
collection := db.C("story.stories")
err := collection.EnsureIndexKey("tags")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("author")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("updatedDate")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("fictionalDate")
if err != nil {
return nil, err
}
err = collection.EnsureIndexKey("listed")
if err != nil {
return nil, err
}
return &storyRepository{
stories: collection,
chapters: db.C("story.chapters"),
comments: db.C("story.comments"),
}, nil
}
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) {
story := new(models.Story)
err := r.stories.FindId(id).One(story)
if err != nil {
return nil, err
}
return story, nil
}
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
query := bson.M{}
if filter.Author != nil {
query["author"] = *filter.Author
}
if filter.Category != nil {
query["category"] = *filter.Category
}
if filter.Open != nil {
query["open"] = *filter.Open
}
if len(filter.Tags) > 0 {
query["tags"] = bson.M{"$all": filter.Tags}
}
if filter.Unlisted != nil {
query["listed"] = !*filter.Unlisted
} else {
query["listed"] = true
}
if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": filter.EarliestFictionalDate,
"$lte": filter.LatestFictionalDate,
}
} else if !filter.EarliestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": filter.EarliestFictionalDate,
}
} else if !filter.LatestFictionalDate.IsZero() {
query["fictionalDate"] = bson.M{
"$lte": filter.LatestFictionalDate,
}
}
stories := make([]*models.Story, 0, 32)
err := r.stories.Find(query).Sort("-updatedDate").Limit(filter.Limit).All(&stories)
if err != nil {
if err == mgo.ErrNotFound {
return stories, nil
}
return nil, err
}
return stories, nil
}
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
story.ID = generate.StoryID()
err := r.stories.Insert(story)
if err != nil {
return nil, err
}
return &story, nil
}
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) {
updateBson := bson.M{}
if update.Name != nil {
updateBson["name"] = *update.Name
story.Name = *update.Name
}
if update.Open != nil {
updateBson["open"] = *update.Open
story.Open = *update.Open
}
if update.Category != nil {
updateBson["category"] = *update.Category
story.Category = *update.Category
}
if update.Author != nil {
updateBson["author"] = *update.Author
story.Author = *update.Author
}
if update.FictionalDate != nil {
updateBson["fictionalDate"] = *update.FictionalDate
story.FictionalDate = *update.FictionalDate
}
if update.UpdatedDate != nil {
updateBson["updatedDate"] = *update.UpdatedDate
story.UpdatedDate = *update.UpdatedDate
}
if update.Listed != nil {
updateBson["listed"] = *update.Listed
story.Listed = *update.Listed
}
if update.SortByFictionalDate != nil {
updateBson["sortByFictionalDate"] = *update.SortByFictionalDate
story.SortByFictionalDate = *update.SortByFictionalDate
}
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
if err != nil {
return nil, err
}
return &story, nil
}
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error {
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$addToSet": bson.M{"tags": tag}})
if err != nil {
return err
}
if ci.Updated == 0 {
return repositories.ErrTagExists
}
return nil
}
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error {
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$pull": bson.M{"tags": tag}})
if err != nil {
return err
}
if ci.Updated == 0 {
return repositories.ErrTagDoesNotExist
}
return nil
}
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error {
err := r.stories.RemoveId(story.ID)
if err != nil {
return err
}
chapterIds := make([]string, 0, 8)
err = r.chapters.Find(bson.M{"storyId": story.ID}).Distinct("_id", &chapterIds)
if err != nil {
log.Println("Failed to find chapterIds:", err)
return nil
}
if len(chapterIds) > 0 {
c1, err := r.chapters.RemoveAll(bson.M{"storyId": story.ID})
if err != nil {
log.Println("Failed to remove chapters:", err)
return nil
}
c2, err := r.comments.RemoveAll(bson.M{"chapterId": bson.M{"$in": chapterIds}})
if err != nil {
log.Println("Failed to remove comments:", err)
return nil
}
log.Printf("Removed story %s (%d chapters, %d comments)", story.ID, c1.Removed, c2.Removed)
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package mongodb
import (
"context"
"sort"
"strings"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
"go.mongodb.org/mongo-driver/bson"
)
type tagRepository struct {
stories *mgo.Collection
}
func newTagRepository(db *mgo.Database) repositories.TagRepository {
return &tagRepository{
stories: db.C("story.stories"),
}
}
func (r *tagRepository) Find(ctx context.Context, kind models.TagKind, name string) (*models.Tag, error) {
tags := make([]*models.Tag, 0, 1)
err := r.stories.Find(bson.M{"listed": true, "tags": bson.M{"kind": kind, "name": name}}).Distinct("tag", &tags)
if err != nil {
return nil, err
} else if len(tags) == 0 {
return nil, repositories.ErrNotFound
}
for _, tag := range tags {
if tag.Kind == kind && tag.Name == name {
return tag, nil
}
}
return nil, repositories.ErrNotFound
}
func (r *tagRepository) List(ctx context.Context, filter models.TagFilter) ([]*models.Tag, error) {
tags := make([]*models.Tag, 0, 64)
query := bson.M{"listed": true, "tags": bson.M{"$ne": nil}}
if filter.Kind != nil {
query["tags.kind"] = *filter.Kind
}
err := r.stories.Find(query).Distinct("tags", &tags)
if err != nil {
return nil, err
}
if filter.Kind != nil {
// While it's unsorted, delete any incorrect tags with replacement.
for i := 0; i < len(tags); i++ {
if tags[i].Kind != *filter.Kind {
tags[i] = tags[len(tags)-1]
tags = tags[:len(tags)-1]
i--
}
}
}
sort.Slice(tags, func(i, j int) bool {
if filter.Kind == nil {
kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind))
if kindCmp != 0 {
return kindCmp < 0
}
}
return strings.Compare(tags[i].Name, tags[j].Name) < 0
})
return tags, nil
}
+41
View File
@@ -0,0 +1,41 @@
package mongodb
import (
"context"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/repositories"
"github.com/globalsign/mgo"
)
type userRepository struct {
users *mgo.Collection
}
func newUserRepository(db *mgo.Database) (repositories.UserRepository, error) {
collection := db.C("core.users")
return &userRepository{users: collection}, nil
}
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
user := new(models.User)
err := r.users.FindId(id).One(user)
if err != nil {
if err == mgo.ErrNotFound {
return nil, repositories.ErrNotFound
}
return nil, err
}
return user, nil
}
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
err := r.users.Insert(user)
if err != nil {
return nil, err
}
return &user, nil
}
+35
View File
@@ -0,0 +1,35 @@
module git.aiterp.net/rpdata/api
go 1.12
require (
github.com/99designs/gqlgen v0.9.0
github.com/beorn7/perks v1.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e
github.com/go-ini/ini v1.35.0 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/h2non/filetype v1.0.8
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect
github.com/prometheus/client_golang v0.9.1
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/common v0.4.0 // indirect
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
github.com/stretchr/testify v1.3.0
github.com/tidwall/pretty v1.0.0 // indirect
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser v1.1.2
go.mongodb.org/mongo-driver v1.0.3
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/text v0.3.0 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.2.2
)
replace golang.org/x/net => golang.org/x/net v0.0.0-20180416171110-a35a21de978d
+142
View File
@@ -0,0 +1,142 @@
github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE=
github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY=
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e h1:hmrbHva/wKD6X4M7pgHVbg/KfVV1wwgr1NEkQaVD3rU=
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-ini/ini v1.35.0 h1:D/my3+xOfqZMkJpciRcyqU7XMBUgiZa9qXjZIa8uv2k=
github.com/go-ini/ini v1.35.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14=
github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05 h1:YEyFCqmHcmbcTLqZ7yEAg7VIUat6PuDzGbIctZy888k=
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e h1:zK8d1aZ+gw/Ne4uMfZTFRxj08PUOp+gGwm4HWUeGI1k=
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180416171110-a35a21de978d h1:O2P57H5Cc+d+DJos+iweraI9rmzMYwV+45vkZdDY0Oo=
golang.org/x/net v0.0.0-20180416171110-a35a21de978d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
+186
View File
@@ -0,0 +1,186 @@
package graph2
import (
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
func complexity() (cr graphcore.ComplexityRoot) {
subListComplexity := 25
bigSublistComplexity := 75
hugeSublistComplexity := 100
findComplexity := 100
listComplexity := 200
mutationComplexity := 100
cr.Query.Character = func(childComplexity int, id *string, nick *string) int {
return childComplexity + findComplexity
}
cr.Query.Characters = func(childComplexity int, filter *models.CharacterFilter) int {
return childComplexity + listComplexity
}
cr.Query.Channel = func(childComplexity int, name string) int {
return childComplexity + findComplexity
}
cr.Query.Channels = func(childComplexity int, filter *models.ChannelFilter) int {
return childComplexity + listComplexity
}
cr.Query.Post = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Posts = func(childComplexity int, filter *models.PostFilter) int {
return childComplexity + listComplexity
}
cr.Query.UnknownNicks = func(childComplexity int, filter *graphcore.UnknownNicksFilter) int {
return childComplexity + listComplexity
}
cr.Query.Log = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Logs = func(childComplexity int, filter *models.LogFilter) int {
if filter != nil && ((filter.Open != nil && *filter.Open == true) || (filter.Limit <= 10)) {
return childComplexity + findComplexity
}
return childComplexity + listComplexity
}
cr.Query.Chapter = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Comment = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Tags = func(childComplexity int, filter *graphcore.TagFilter) int {
return childComplexity + listComplexity
}
cr.Query.Story = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Stories = func(childComplexity int, filter *models.StoryFilter) int {
if filter != nil && filter.Limit <= 10 {
return childComplexity + findComplexity
}
return childComplexity + listComplexity
}
cr.Query.File = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Files = func(childComplexity int, filter *models.FileFilter) int {
return childComplexity + listComplexity
}
cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int {
return childComplexity + listComplexity
}
cr.Query.Token = func(childComplexity int) int {
return childComplexity + findComplexity
}
cr.Mutation.AddStory = func(childComplexity int, input graphcore.StoryAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddStoryTag = func(childComplexity int, input graphcore.StoryTagAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveStoryTag = func(childComplexity int, input graphcore.StoryTagRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditStory = func(childComplexity int, input graphcore.StoryEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveStory = func(childComplexity int, input graphcore.StoryRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddChapter = func(childComplexity int, input graphcore.ChapterAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditChapter = func(childComplexity int, input graphcore.ChapterEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.MoveChapter = func(childComplexity int, input graphcore.ChapterMoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveChapter = func(childComplexity int, input graphcore.ChapterRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddComment = func(childComplexity int, input graphcore.CommentAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditComment = func(childComplexity int, input graphcore.CommentEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveComment = func(childComplexity int, input graphcore.CommentRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddLog = func(childComplexity int, input graphcore.LogAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.ImportLog = func(childComplexity int, input graphcore.LogImportInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditLog = func(childComplexity int, input graphcore.LogEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveLog = func(childComplexity int, input graphcore.LogRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddPost = func(childComplexity int, input graphcore.PostAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditPost = func(childComplexity int, input graphcore.PostEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.MovePost = func(childComplexity int, input graphcore.PostMoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemovePost = func(childComplexity int, input graphcore.PostRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddCharacter = func(childComplexity int, input graphcore.CharacterAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditCharacter = func(childComplexity int, input graphcore.CharacterEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveCharacter = func(childComplexity int, input graphcore.CharacterRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddChannel = func(childComplexity int, input graphcore.ChannelAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditChannel = func(childComplexity int, input graphcore.ChannelEditInput) int {
return childComplexity + mutationComplexity
}
cr.Log.Characters = func(childComplexity int) int {
return childComplexity + subListComplexity
}
cr.Log.Posts = func(childComplexity int, kinds []string) int {
return childComplexity + hugeSublistComplexity
}
cr.Log.NextLogs = func(childComplexity int) int {
return childComplexity + (subListComplexity / 2)
}
cr.Log.PrevLogs = func(childComplexity int) int {
return childComplexity + (subListComplexity / 2)
}
cr.Story.Chapters = func(childComplexity int) int {
return childComplexity + bigSublistComplexity
}
cr.Story.Tags = func(childComplexity int) int {
return childComplexity + subListComplexity
}
cr.Chapter.Comments = func(childComplexity int, limit *int) int {
return childComplexity + subListComplexity
}
return
}
+71
View File
@@ -0,0 +1,71 @@
schema:
- schema/root.gql
- schema/types/*.gql
exec:
filename: graphcore/exec_gen.go
package: graphcore
model:
filename: graphcore/input_gen.go
package: graphcore
models:
Tag:
model: git.aiterp.net/rpdata/api/models.Tag
TagInput: #It's the same as tags
model: git.aiterp.net/rpdata/api/models.Tag
TagKind:
model: git.aiterp.net/rpdata/api/models.TagKind
Character:
model: git.aiterp.net/rpdata/api/models.Character
CharactersFilter:
model: git.aiterp.net/rpdata/api/models.CharacterFilter
Channel:
model: git.aiterp.net/rpdata/api/models.Channel
ChannelsFilter:
model: git.aiterp.net/rpdata/api/models.ChannelFilter
UnknownNick:
model: git.aiterp.net/rpdata/api/models.UnknownNick
Post:
model: git.aiterp.net/rpdata/api/models.Post
PostsFilter:
model: git.aiterp.net/rpdata/api/models.PostFilter
Log:
model: git.aiterp.net/rpdata/api/models.Log
LogsFilter:
model: git.aiterp.net/rpdata/api/models.LogFilter
LogImporter:
model: git.aiterp.net/rpdata/api/models.LogImporter
LogSuggestion:
model: git.aiterp.net/rpdata/api/models.LogSuggestion
Comment:
model: git.aiterp.net/rpdata/api/models.Comment
ChapterCommentMode:
model: git.aiterp.net/rpdata/api/models.ChapterCommentMode
Chapter:
model: git.aiterp.net/rpdata/api/models.Chapter
Story:
model: git.aiterp.net/rpdata/api/models.Story
StoryCategory:
model: git.aiterp.net/rpdata/api/models.StoryCategory
StoriesFilter:
model: git.aiterp.net/rpdata/api/models.StoryFilter
File:
model: git.aiterp.net/rpdata/api/models.File
FilesFilter:
model: git.aiterp.net/rpdata/api/models.FileFilter
Change:
model: git.aiterp.net/rpdata/api/models.Change
ChangeModel:
model: git.aiterp.net/rpdata/api/models.ChangeModel
ChangeKey:
model: git.aiterp.net/rpdata/api/models.ChangeKey
ChangeKeyInput: # It's the same as ChangeKey
model: git.aiterp.net/rpdata/api/models.ChangeKey
ChangesFilter:
model: git.aiterp.net/rpdata/api/models.ChangeFilter
Token:
model: git.aiterp.net/rpdata/api/models.Token
User:
model: git.aiterp.net/rpdata/api/models.User
+59
View File
@@ -0,0 +1,59 @@
package graph2
import (
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/graph2/resolvers"
"git.aiterp.net/rpdata/api/graph2/types"
"git.aiterp.net/rpdata/api/services"
graphql "github.com/99designs/gqlgen/graphql"
)
//go:generate go run github.com/99designs/gqlgen -v
// New creates a new GraphQL schema.
func New(s *services.Bundle) graphql.ExecutableSchema {
return graphcore.NewExecutableSchema(graphcore.Config{
Resolvers: &rootResolver{s},
Complexity: complexity(),
})
}
type rootResolver struct {
s *services.Bundle
}
func (r *rootResolver) Query() graphcore.QueryResolver {
return resolvers.QueryResolver(r.s)
}
func (r *rootResolver) Mutation() graphcore.MutationResolver {
return resolvers.MutationResolver(r.s)
}
func (r *rootResolver) Subscription() graphcore.SubscriptionResolver {
return resolvers.SubscriptionResolver(r.s)
}
func (r *rootResolver) Log() graphcore.LogResolver {
return types.LogResolver(r.s)
}
func (r *rootResolver) Comment() graphcore.CommentResolver {
return types.CommentResolver(r.s)
}
func (r *rootResolver) Chapter() graphcore.ChapterResolver {
return types.ChapterResolver(r.s)
}
func (r *rootResolver) Story() graphcore.StoryResolver {
return types.StoryResolver(r.s)
}
func (r *rootResolver) Change() graphcore.ChangeResolver {
return &types.ChangeResolver
}
func (r *rootResolver) Token() graphcore.TokenResolver {
return types.TokenResolver(r.s)
}
+26
View File
@@ -0,0 +1,26 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/models"
)
/// Queries
func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) {
if filter == nil {
filter = &models.ChangeFilter{}
}
return r.s.Changes.List(ctx, *filter)
}
/// Subscriptions
func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) {
if filter == nil {
filter = &models.ChangeFilter{}
}
return r.s.Changes.Subscribe(ctx, *filter), nil
}
+49
View File
@@ -0,0 +1,49 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Channel(ctx context.Context, name string) (*models.Channel, error) {
return r.s.Channels.Find(ctx, name)
}
func (r *queryResolver) Channels(ctx context.Context, filter *models.ChannelFilter) ([]*models.Channel, error) {
if filter == nil {
filter = &models.ChannelFilter{}
}
return r.s.Channels.List(ctx, *filter)
}
// Mutations
func (r *mutationResolver) AddChannel(ctx context.Context, input graphcore.ChannelAddInput) (*models.Channel, error) {
logged := input.Logged != nil && *input.Logged
hub := input.Hub != nil && *input.Hub
locationName := ""
if input.LocationName != nil {
locationName = *input.LocationName
}
eventName := ""
if input.LocationName != nil {
eventName = *input.EventName
}
return r.s.Channels.Create(ctx, input.Name, logged, hub, eventName, locationName)
}
func (r *mutationResolver) EditChannel(ctx context.Context, input graphcore.ChannelEditInput) (*models.Channel, error) {
return r.s.Channels.Update(ctx, input.Name, models.ChannelUpdate{
Logged: input.Logged,
Hub: input.Hub,
EventName: input.EventName,
LocationName: input.LocationName,
})
}
+71
View File
@@ -0,0 +1,71 @@
package resolvers
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Chapter(ctx context.Context, id string) (*models.Chapter, error) {
return r.s.Stories.FindChapter(ctx, id)
}
// Mutations
func (r *mutationResolver) AddChapter(ctx context.Context, input graphcore.ChapterAddInput) (*models.Chapter, error) {
story, err := r.s.Stories.FindStory(ctx, input.StoryID)
if err != nil {
return nil, err
}
commentMode := models.ChapterCommentModeDisabled
if input.CommentMode != nil {
commentMode = *input.CommentMode
}
return r.s.Stories.CreateChapter(ctx, *story, input.Title, input.Source, input.Author, time.Now(), input.FictionalDate, commentMode)
}
func (r *mutationResolver) MoveChapter(ctx context.Context, input graphcore.ChapterMoveInput) (*models.Chapter, error) {
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
if err != nil {
return nil, err
}
from, err := r.s.Stories.FindStory(ctx, chapter.StoryID)
if err != nil {
return nil, err
}
to, err := r.s.Stories.FindStory(ctx, input.StoryID)
if err != nil {
return nil, err
}
return r.s.Stories.MoveChapter(ctx, chapter, *from, *to)
}
func (r *mutationResolver) EditChapter(ctx context.Context, input graphcore.ChapterEditInput) (*models.Chapter, error) {
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
if err != nil {
return nil, err
}
return r.s.Stories.EditChapter(ctx, chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked)
}
func (r *mutationResolver) RemoveChapter(ctx context.Context, input graphcore.ChapterRemoveInput) (*models.Chapter, error) {
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
if err != nil {
return nil, err
}
err = r.s.Stories.RemoveChapter(ctx, chapter)
if err != nil {
return nil, err
}
return chapter, nil
}
+65
View File
@@ -0,0 +1,65 @@
package resolvers
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Character(ctx context.Context, id *string, nick *string) (*models.Character, error) {
if id != nil {
return r.s.Characters.Find(ctx, *id)
} else if nick != nil {
return r.s.Characters.FindNick(ctx, *nick)
} else {
return nil, errors.New("You must specify either an ID or a nick")
}
}
func (r *queryResolver) Characters(ctx context.Context, filter *models.CharacterFilter) ([]*models.Character, error) {
if filter == nil {
filter = &models.CharacterFilter{}
}
return r.s.Characters.List(ctx, *filter)
}
// Mutations
func (r *mutationResolver) AddCharacter(ctx context.Context, input graphcore.CharacterAddInput) (*models.Character, error) {
author := ""
if input.Author != nil {
author = *input.Author
}
shortName := ""
if input.ShortName != nil {
shortName = *input.ShortName
}
description := ""
if input.Description != nil {
description = *input.Description
}
return r.s.Characters.Create(ctx, input.Nick, input.Name, shortName, author, description)
}
func (r *mutationResolver) AddCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
return r.s.Characters.AddNick(ctx, input.ID, input.Nick)
}
func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
return r.s.Characters.RemoveNick(ctx, input.ID, input.Nick)
}
func (r *mutationResolver) EditCharacter(ctx context.Context, input graphcore.CharacterEditInput) (*models.Character, error) {
return r.s.Characters.Update(ctx, input.ID, input.Name, input.ShortName, input.Description)
}
func (r *mutationResolver) RemoveCharacter(ctx context.Context, input graphcore.CharacterRemoveInput) (*models.Character, error) {
return r.s.Characters.Delete(ctx, input.ID)
}
+64
View File
@@ -0,0 +1,64 @@
package resolvers
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Comment(ctx context.Context, id string) (*models.Comment, error) {
return r.s.Stories.FindComment(ctx, id)
}
// Mutations
func (r *mutationResolver) AddComment(ctx context.Context, input graphcore.CommentAddInput) (*models.Comment, error) {
chapter, err := r.s.Stories.FindChapter(ctx, input.ChapterID)
if err != nil {
return nil, err
}
subject := ""
if input.Subject != nil {
subject = *input.Subject
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
fictionalDate = *input.FictionalDate
}
return r.s.Stories.CreateComment(ctx, *chapter, subject, "", input.Source, input.CharacterName, input.CharacterID, time.Now(), fictionalDate)
}
func (r *mutationResolver) EditComment(ctx context.Context, input graphcore.CommentEditInput) (*models.Comment, error) {
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
if err != nil {
return nil, err
}
fictionalDate := input.FictionalDate
if input.ClearFictionalDate != nil && *input.ClearFictionalDate {
fictionalDate = &time.Time{}
}
return r.s.Stories.EditComment(ctx, comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, fictionalDate)
}
func (r *mutationResolver) RemoveComment(ctx context.Context, input graphcore.CommentRemoveInput) (*models.Comment, error) {
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
if err != nil {
return nil, err
}
err = r.s.Stories.RemoveComment(ctx, comment)
if err != nil {
return nil, err
}
return comment, nil
}
+39
View File
@@ -0,0 +1,39 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) File(ctx context.Context, id string) (*models.File, error) {
return r.s.Files.Find(ctx, id)
}
func (r *queryResolver) Files(ctx context.Context, filter *models.FileFilter) ([]*models.File, error) {
if filter == nil {
public := true
filter = &models.FileFilter{
Public: &public,
}
}
return r.s.Files.List(ctx, *filter)
}
// Mutations
func (r *mutationResolver) UploadFile(ctx context.Context, input *graphcore.FileUploadInput) (*models.File, error) {
return r.s.Files.Upload(ctx, input.File.File, input.Name, input.Public, input.File.Size)
}
func (r *mutationResolver) EditFile(ctx context.Context, input *graphcore.FileEditInput) (*models.File, error) {
return r.s.Files.Edit(ctx, input.ID, input.Name, input.Public)
}
func (r *mutationResolver) RemoveFile(ctx context.Context, input *graphcore.FileRemoveInput) (*models.File, error) {
return r.s.Files.Remove(ctx, input.ID)
}
+89
View File
@@ -0,0 +1,89 @@
package resolvers
import (
"context"
"errors"
"time"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Log(ctx context.Context, id string) (*models.Log, error) {
return r.s.Logs.Find(ctx, id)
}
func (r *queryResolver) Logs(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
return r.s.Logs.List(ctx, filter)
}
// Mutations
func (r *mutationResolver) AddLog(ctx context.Context, input graphcore.LogAddInput) (*models.Log, error) {
open := input.Open != nil && *input.Open == true
title := ""
if input.Title != nil {
title = *input.Title
}
event := ""
if input.Event != nil {
event = *input.Event
}
description := ""
if input.Description != nil {
description = *input.Description
}
return r.s.Logs.Create(ctx, title, description, input.Channel, event, open)
}
func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImportInput) ([]*models.Log, error) {
date := time.Time{}
if input.Date != nil {
date = *input.Date
}
tz := time.UTC
if input.Timezone != nil {
parsedTZ, err := time.LoadLocation(*input.Timezone)
if err != nil {
return nil, errors.New("Unknown timezone: " + *input.Timezone)
}
tz = parsedTZ
}
sessionThreshold := time.Hour * 12
if input.SessionThresholdMs != nil {
sessionThreshold = time.Duration(*input.SessionThresholdMs) * time.Millisecond
}
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, sessionThreshold, input.Data)
}
func (r *mutationResolver) SplitLog(ctx context.Context, input graphcore.LogSplitInput) (*models.Log, error) {
return r.s.Logs.SplitLog(ctx, input.LogID, input.StartPostID)
}
func (r *mutationResolver) MergeLog(ctx context.Context, input graphcore.LogMergeInput) (*models.Log, error) {
removeAfter := input.RemoveAfter != nil && *input.RemoveAfter
return r.s.Logs.MergeLogs(ctx, input.TargetLogID, input.SourceLogID, removeAfter)
}
func (r *mutationResolver) EditLog(ctx context.Context, input graphcore.LogEditInput) (*models.Log, error) {
update := models.LogUpdate{
Open: input.Open,
Description: input.Description,
EventName: input.Event,
Title: input.Title,
}
return r.s.Logs.Update(ctx, input.ID, update)
}
func (r *mutationResolver) RemoveLog(ctx context.Context, input graphcore.LogRemoveInput) (*models.Log, error) {
return r.s.Logs.Delete(ctx, input.ID)
}
+40
View File
@@ -0,0 +1,40 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *queryResolver) Post(ctx context.Context, id string) (*models.Post, error) {
return r.s.Logs.FindPosts(ctx, id)
}
func (r *queryResolver) Posts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
return r.s.Logs.ListPosts(ctx, filter)
}
// Mutation
func (r *mutationResolver) AddPost(ctx context.Context, input graphcore.PostAddInput) (*models.Post, error) {
return r.s.Logs.AddPost(ctx, input.LogID, input.Time, input.Kind, input.Nick, input.Text)
}
func (r *mutationResolver) EditPost(ctx context.Context, input graphcore.PostEditInput) (*models.Post, error) {
return r.s.Logs.EditPost(ctx, input.ID, models.PostUpdate{
Time: input.Time,
Kind: input.Kind,
Nick: input.Nick,
Text: input.Text,
})
}
func (r *mutationResolver) MovePost(ctx context.Context, input graphcore.PostMoveInput) ([]*models.Post, error) {
return r.s.Logs.MovePost(ctx, input.ID, input.ToPosition)
}
func (r *mutationResolver) RemovePost(ctx context.Context, input graphcore.PostRemoveInput) (*models.Post, error) {
return r.s.Logs.DeletePost(ctx, input.ID)
}
+26
View File
@@ -0,0 +1,26 @@
package resolvers
import (
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/services"
)
type queryResolver struct{ s *services.Bundle }
type mutationResolver struct{ s *services.Bundle }
type subscriptionResolver struct{ s *services.Bundle }
// QueryResolver has all the queries
func QueryResolver(s *services.Bundle) graphcore.QueryResolver {
return &queryResolver{s: s}
}
// MutationResolver brings the radioactive goop
func MutationResolver(s *services.Bundle) graphcore.MutationResolver {
return &mutationResolver{s: s}
}
// SubscriptionResolver has the real-time magic.
func SubscriptionResolver(s *services.Bundle) graphcore.SubscriptionResolver {
return &subscriptionResolver{s: s}
}
+83
View File
@@ -0,0 +1,83 @@
package resolvers
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
func (r *queryResolver) Story(ctx context.Context, id string) (*models.Story, error) {
return r.s.Stories.FindStory(ctx, id)
}
func (r *queryResolver) Stories(ctx context.Context, filter *models.StoryFilter) ([]*models.Story, error) {
if filter == nil {
filter = &models.StoryFilter{}
}
return r.s.Stories.ListStories(ctx, *filter)
}
// Mutations
func (r *mutationResolver) AddStory(ctx context.Context, input graphcore.StoryAddInput) (*models.Story, error) {
listed := input.Listed != nil && *input.Listed
open := input.Open != nil && *input.Open
tags := make([]models.Tag, len(input.Tags))
for i, tag := range input.Tags {
tags[i] = *tag
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
fictionalDate = *input.FictionalDate
}
sortByFictionalDate := input.SortByFictionalDate != nil && *input.SortByFictionalDate
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate, sortByFictionalDate)
}
func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) {
story, err := r.s.Stories.FindStory(ctx, input.ID)
if err != nil {
return nil, err
}
return r.s.Stories.AddStoryTag(ctx, *story, *input.Tag)
}
func (r *mutationResolver) RemoveStoryTag(ctx context.Context, input graphcore.StoryTagRemoveInput) (*models.Story, error) {
story, err := r.s.Stories.FindStory(ctx, input.ID)
if err != nil {
return nil, err
}
return r.s.Stories.RemoveStoryTag(ctx, *story, *input.Tag)
}
func (r *mutationResolver) EditStory(ctx context.Context, input graphcore.StoryEditInput) (*models.Story, error) {
story, err := r.s.Stories.FindStory(ctx, input.ID)
if err != nil {
return nil, err
}
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate, input.SortByFictionalDate)
}
func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) {
story, err := r.s.Stories.FindStory(ctx, input.ID)
if err != nil {
return nil, err
}
err = r.s.Stories.RemoveStory(ctx, story)
if err != nil {
return nil, err
}
return story, err
}
+17
View File
@@ -0,0 +1,17 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
func (r *queryResolver) Tags(ctx context.Context, filter *graphcore.TagFilter) ([]*models.Tag, error) {
tagFilter := models.TagFilter{}
if filter != nil && filter.Kind != nil {
tagFilter.Kind = filter.Kind
}
return r.s.Tags.ListTags(ctx, tagFilter)
}
+17
View File
@@ -0,0 +1,17 @@
package resolvers
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/models"
)
func (r *queryResolver) Token(ctx context.Context) (*models.Token, error) {
token := r.s.Auth.TokenFromContext(ctx)
if !token.Authenticated() {
return nil, errors.New("no (valid) token")
}
return token, nil
}
+24
View File
@@ -0,0 +1,24 @@
package resolvers
import (
"context"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
/// Queries
func (r *queryResolver) UnknownNicks(ctx context.Context, filter *graphcore.UnknownNicksFilter) ([]*models.UnknownNick, error) {
limit := 20
if filter != nil {
limit = filter.Limit
}
nicks := r.s.Logs.ListUnknownNicks()
if limit > 0 && len(nicks) > limit {
nicks = nicks[:limit]
}
return nicks, nil
}
+190
View File
@@ -0,0 +1,190 @@
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Query {
# Find character by either an ID or a nick.
character(id: String, nick: String): Character!
# Find characters
characters(filter: CharactersFilter): [Character!]!
# Find channel by name
channel(name: String!): Channel!
# Find channels
channels(filter: ChannelsFilter): [Channel!]!
# Find post by ID.
post(id: String!): Post!
# Find posts
posts(filter: PostsFilter): [Post!]!
"List unknown nicks"
unknownNicks(filter: UnknownNicksFilter): [UnknownNick!]!
# Find log by ID
log(id: String!): Log!
# Find logs
logs(filter: LogsFilter): [Log!]!
# Find story chapter by ID
chapter(id: String!): Chapter!
# Find comment by ID
comment(id: String!): Comment!
# Find all distinct tags used in stories
tags(filter: TagFilter): [Tag!]!
# Find story by ID
story(id: String!): Story!
# Find stories
stories(filter: StoriesFilter): [Story!]!
# Find file by ID
file(id: String!): File!
# Find files
files(filter: FilesFilter): [File!]!
# Find changes
changes(filter: ChangesFilter): [Change!]!
# Get information about the token, useful for debugging.
token: Token!
}
type Mutation {
# Add a story
addStory(input: StoryAddInput!): Story!
# Add a story tag
addStoryTag(input: StoryTagAddInput!): Story!
# Remove a story tag
removeStoryTag(input: StoryTagRemoveInput!): Story!
# Edit a story
editStory(input: StoryEditInput!): Story!
# Remove a story
removeStory(input: StoryRemoveInput!): Story!
# Add a chapter to a story
addChapter(input: ChapterAddInput!): Chapter!
# Edit a chapter
editChapter(input: ChapterEditInput!): Chapter!
# Move a chapter
moveChapter(input: ChapterMoveInput!): Chapter!
# Remove a chapter
removeChapter(input: ChapterRemoveInput!): Chapter!
"Add a comment to a chapter."
addComment(input: CommentAddInput!): Comment!
"Edit a comment in a chapter."
editComment(input: CommentEditInput!): Comment!
"Remove a comment in a chapter."
removeComment(input: CommentRemoveInput!): Comment!
# Add a new log
addLog(input: LogAddInput!): Log!
# Import a log
importLog(input: LogImportInput!): [Log!]!
# Split a log
splitLog(input: LogSplitInput!): Log!
# Merge a log
mergeLog(input: LogMergeInput!): Log!
# Edit a log
editLog(input: LogEditInput!): Log!
# Remove a log
removeLog(input: LogRemoveInput!): Log!
# Add a post
addPost(input: PostAddInput!): Post!
# Edit a post
editPost(input: PostEditInput!): Post!
# Move a post. All affected posts will be returned.
movePost(input: PostMoveInput!): [Post!]!
# Remove a post
removePost(input: PostRemoveInput!): Post!
# Add a new character
addCharacter(input: CharacterAddInput!): Character!
# Add nick to character
addCharacterNick(input: CharacterNickInput!): Character!
# Remove nick from character
removeCharacterNick(input: CharacterNickInput!): Character!
# Edit character
editCharacter(input: CharacterEditInput!): Character!
# Remove a character
removeCharacter(input: CharacterRemoveInput!): Character!
# Add a channel
addChannel(input: ChannelAddInput!): Channel!
# Edit a channel
editChannel(input: ChannelEditInput!): Channel!
"Upload a file"
uploadFile(input: FileUploadInput): File!
"Edit a file"
editFile(input: FileEditInput): File!
"Remove a file"
removeFile(input: FileRemoveInput): File!
}
type Subscription {
"""
Changes subscribes to the changes matching the following keys.
"""
changes(filter: ChangesFilter): Change!
}
# A Time represents a RFC3339 encoded date with up to millisecond precision.
scalar Time
"""A GraphQL file upload."""
scalar Upload
+86
View File
@@ -0,0 +1,86 @@
"""
A change represents a a change in the history.
"""
type Change {
"A unique ID of the change."
id: String!
"The model of the changed object."
model: ChangeModel!
"The operation performed."
op: String!
"The author that performed the change."
author: String!
"Whether the change will be listed without a matching key."
listed: Boolean!
"The keys indexing this change."
keys: [ChangeKey!]!
"The date when the change happened."
date: Time
"The changed objects."
objects: [ChangeObject!]!
}
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment | File
"""
A change key is a key associated with a change, used for filtering
and subscription. A log post edit has keys for both the log and post,
making it suitable to use it to patch an active log file.
"""
type ChangeKey {
"The model the key is for."
model: ChangeModel!
"The model's id, or * if it's a key indicating a model list should be updated."
id: String!
}
"""
See ChangeKey
"""
input ChangeKeyInput {
"The model the key is for."
model: ChangeModel!
"The model's id, or * if it's a key indicating a model list should be updated."
id: String!
}
"""
A model related to the change.
"""
enum ChangeModel {
Character
Channel
Log
Post
Story
Tag
Chapter
Comment
File
}
"""
Optional filter for the changes query.
"""
input ChangesFilter {
"The keys to query for."
keys: [ChangeKeyInput!]
"Only show changes by this author"
author: String
"Only show changes more recent than this date."
earliestDate: Time
"Limit the amount of results. This even goes for subscriptions!"
limit: Int
}
@@ -16,6 +16,18 @@ type Channel {
locationName: String
}
# Filters for the channels query
input ChannelsFilter {
# Filter to either logged or unlogged channels
logged: Boolean
# Filter by event name
eventName: String
# Filter by location name
locationName: String
}
input ChannelAddInput {
# The channel's name
name: String!
+115
View File
@@ -0,0 +1,115 @@
# A Chapter is the main content body of a story.
type Chapter {
# The chapter's ID
id: String!
# The chapter's title
title: String!
# The chapter's author
author: String!
# The chapter's source
source: String!
# When the chapter was posted initialy
createdDate: Time!
# The in-universe date and time of the chapter
fictionalDate: Time
# The date of edit.
editedDate: Time!
"The comment mode."
commentMode: ChapterCommentMode!
"Whether the author has locket comments."
commentsLocked: Boolean!
"A shorthand for checking whether a logged-in user can comment on this chapter."
canComment: Boolean!
"Get all chapter comments."
comments(limit: Int): [Comment!]!
}
"""
The possibles modes of comments for a chapter.
"""
enum ChapterCommentMode {
"Comments are disabled and hidden."
Disabled
"Comments should be shown as typical news article comments."
Article
"Comments should be shown in a compact form suitable for many small messages."
Chat
"Comments are going to be shown as omni-tool messages."
Message
}
# Input for addChapter mutation
input ChapterAddInput {
# The story to add a chapter to
storyId: String!
# The title to give the chapter. It can not be left empty
title: String!
# The author to set for the chapter. It will use the logged in user ID, and only
# users with appropriate permissions can post as another author. Even then, it's only
# for importing/moving content. This author name will not be used when logging the
# submission
author: String
# The markdown source for the content
source: String!
# Optionally, assign a fictional date for the chapter
fictionalDate: Time
"The comment mode for the chapter."
commentMode: ChapterCommentMode
}
# Input for editChapter mutation
input ChapterEditInput {
# The chapter to edit
id: String!
# Change the chapter's title
title: String
# Change the source
source: String
# Set the fictional date for a chapter
fictionalDate: Time
# Remove the fictional date for a chapter
clearFictionalDate: Boolean
"Set the comment mode for the chapter"
commentMode: ChapterCommentMode
"Set whether the chapter comments are locked"
commentsLocked: Boolean
}
# Input for moveChapter mutation
input ChapterMoveInput {
# The chapter to move
id: String!
# The story to move it to
storyId: String!
}
# Input for removeChapter mutation
input ChapterRemoveInput {
# The chapter to remove
id: String!
}
@@ -22,6 +22,24 @@ type Character {
description: String!
}
# Filter for characters query
input CharactersFilter {
# Filter by character IDs
ids: [String!]
# Filter by nicks
nicks: [String!]
# Filter by names
names: [String!]
# Filter by author
author: String
# Filter by text search matching against the character's description
search: String
}
# Input for adding characters
input CharacterAddInput {
# The primary IRC nick name to recognize this character by
@@ -50,6 +68,7 @@ input CharacterNickInput {
nick: String!
}
# Inpuit for editCharacter mutation
input CharacterEditInput {
# The id for the character to edit
id: String!
@@ -62,4 +81,11 @@ input CharacterEditInput {
# A short description for the character
description: String
}
}
# Input for removeCharacter mutation
input CharacterRemoveInput {
# The id for the character to edit
id: String!
}
+91
View File
@@ -0,0 +1,91 @@
"""
A Comment represents a comment to a story chapter.
"""
type Comment {
"A unique ID of the change."
id: String!
"subject"
subject: String!
"The chapter ID the comment belongs to."
chapterId: String!
"The comment's author."
author: String!
"The displayed name of the character. This may be the same as character.name, but it does not need to be."
characterName: String!
"The character associated with the comment."
character: Character
"The fictional (IC) date of the comment."
fictionalDate: Time
"The date of creation."
createdDate: Time!
"The date of the last edit."
editedDate: Time!
"The markdown source of the comment."
source: String!
}
"""
Input for the addComment mutation
"""
input CommentAddInput {
"What chapter to comment on."
chapterId: String!
"The markdown source of the comment."
source: String!
"The name of the character associated with this comment."
characterName: String!
"Optioanlly, a character ID."
characterId: String
"Optional in-universe date of the comment."
fictionalDate: Time
"Optional subject to add to the comment, only shown in some modes."
subject: String
}
"""
Input for the editComment mutation
"""
input CommentEditInput {
"What comment to edit."
commentId: String!
"Change the markdown source."
source: String
"Change the name of the character associated with this comment."
characterName: String
"Change the character assocaited with this comment."
characterId: String
"Change the optional in-universe date of the comment."
fictionalDate: Time
"Set to clear the fictional date."
clearFictionalDate: Boolean
"Change the optional subject for the comment, only shown in some modes."
subject: String
}
"""
Input for the removeComment mutation
"""
input CommentRemoveInput {
"What comment to edit."
commentId: String!
}
@@ -7,7 +7,7 @@ type File {
kind: String!
# The time of uploading
time: String!
time: Time!
# Whether the file is publicly listable. Someone with knowledge of the ID
# will still be able to view it, however.
@@ -29,9 +29,30 @@ type File {
url: String
}
# Filter for the files quiery.
input FilesFilter {
# If set, this will limit the results to either public or private files.
public: Boolean
# Limit the MIME types of the files.
mimeTypes: [String!]
}
# Input for uploadFile mutation
input FileUploadInput {
"The file name"
name: String!
# Whether the file should be public
public: Boolean!
# The actual file upload
file: Upload!
}
# Input for editFile mutation
input EditFileInput {
# The file's unique ID
input FileEditInput {
# The file's unique ID
id: String!
# Whether the file is publicly listable. Someone with knowledge of the ID
@@ -40,4 +61,10 @@ input EditFileInput {
# The file's name
name: String
}
# Input for removeFile mutation
input FileRemoveInput {
# The file's unique ID
id: String!
}
@@ -8,7 +8,7 @@ type Log {
shortId: String!
# The date for a log.
date: String!
date: Time!
# The channel of a log.
channelName: String!
@@ -21,7 +21,7 @@ type Log {
# The log's event, which is the same as the tags in the previous logbot site.
# Empty string means that it's no event.
event: String!
eventName: String!
# The description of a session, which is empty if unset.
description: String!
@@ -34,10 +34,24 @@ type Log {
# The posts of the logfile, which can be filtered by kinds.
posts(kinds:[String!]): [Post!]!
# Suggested next logs.
nextLogs: [LogSuggestion!]!
# Suggested previous logs.
prevLogs: [LogSuggestion!]!
}
# Input for logs query
input LogQueryInput {
# Suggested log
type LogSuggestion {
log: Log!
hasEvent: Boolean!
hasChannel: Boolean!
characters: [Character!]!
}
# Filter for logs query
input LogsFilter {
# Channels to limit results to (inclusive)
channels: [String!]
@@ -50,17 +64,17 @@ input LogQueryInput {
# Search post content
search: String
# Limit to only open logs
# Limit by whether it's open or not
open: Boolean
# Limit the amount of posts
# Limit the amount of results you get back
limit: Int
}
# Input for addLog mutation
input LogAddInput {
# The date of the log, in RFC3339 format with up to nanosecond precision
date: String!
# The date of the log.
date: Time!
# The channel where the log is set
channel: String!
@@ -96,37 +110,73 @@ input LogEditInput {
open: Boolean
}
# A LogHeader is a cut-down version of Log that does not allow getting the actual posts.
type LogHeader {
# A unique identifier for the log.
# Input for removeLog mutation
input LogRemoveInput {
# The id of the log to remove
id: String!
}
# A secondary unique identifier for the log. This is a lot shorter and more suitable for storage when
# the link doesn't need to be as expressive.
shortId: String!
# The date for a log.
date: String!
# The channel name of a log.
"""
Input for importLog mutation.
"""
input LogImportInput {
"The channel of the log, alwyas required."
channelName: String!
# Information about the channel this log is in.
channel: Channel!
# The session's title.
title: String!
"Which importer to use."
importer: LogImporter!
# The log's event, which is the same as the tags in the previous logbot site.
# Empty string means that it's no event.
event: String!
"The timezone of the import, all log and post dates will be treated as it. It will be UTC if none is specified."
timezone: String
# The description of a session, which is empty if unset.
description: String!
"The date of the log, if not provided in the log body."
date: Time
# Whether the log session is open.
open: Boolean!
"Session threwhold"
sessionThresholdMs: Int
# The characters involved in the log file.
characters: [Character!]!
"The log body itself."
data: String!
}
"""
Input for splitLog mutation.
"""
input LogSplitInput {
"Log ID"
logId: String!
"Split out all posts from and including the post with this ID"
startPostId: String!
}
"""
Input for mergeLog mutation.
"""
input LogMergeInput {
"Target log ID"
targetLogId: String!
"Source log ID"
sourceLogId: String!
"Remove the source log after merging"
removeAfter: Boolean
}
enum LogImporter {
"""
mIRC-like: This importer parses logs that copied from mIRC posts without spaces.
"""
MircLike
"""
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
"""
ForumLog
"""
IRCCloud: This importer parses irccloud exports.
"""
IrcCloud
}
@@ -7,7 +7,7 @@ type Post {
logId: String!
# The date and time of posting
time: String!
time: Time!
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
kind: String!
@@ -23,12 +23,12 @@ type Post {
}
# Input for the addPost mutation
input AddPostInput {
input PostAddInput {
# The log's ID that this post should be a part of
logId: String!
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
time: String!
time: Time!
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
kind: String!
@@ -41,12 +41,12 @@ input AddPostInput {
}
# Input for the editPost mutation
input EditPostInput {
input PostEditInput {
# The Post ID
id: String!
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
time: String
time: Time
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
kind: String
@@ -59,10 +59,24 @@ input EditPostInput {
}
# Input for the movePost mutation
input MovePostInput {
input PostMoveInput {
# The Post ID
id: String!
# Target index
toPosition: Int!
}
# Input for the removePost mutation
input PostRemoveInput {
# The Post ID
id: String!
}
# Filter for posts query
input PostsFilter {
ids: [String!]
kinds: [String!]
logId: String
limit: Int
}
@@ -25,28 +25,41 @@ type Story {
# The chapters of this story
chapters: [Chapter!]!
# The date the story was created (RFC3339 format).
createdDate: String!
# The date the story was created.
createdDate: Time!
# The date the story is set in (RFC3339 format).
fictionalDate: String!
# The date the story is set in.
fictionalDate: Time
# The date of the last major update to the story (RFC3339 format). This being bumped means a new chapter has been added.
updatedDate: String!
# The date of the last major update to the story. This being bumped means a new chapter has been added.
updatedDate: Time!
# Whether to sort by fictional date.
sortByFictionalDate: Boolean!
}
input StoriesQueryInput {
# Filter for stories query.
input StoriesFilter {
# What author to query for
author: String
# What category to query for
category: StoryCategory
# What tags to query for
tags: [TagInput!]
# If true, it will only show unlisted page you have access to
unlisted: Boolean
# Whether the page is open for additions by other users
open: Boolean
# The earliest fictionalDate
earliestFictionalDate: String
earliestFictionalDate: Time
# The latest fictionalDate
latestFictionalDate: String
latestFictionalDate: Time
# The max amount of stories to get (default: 30)
limit: Int
@@ -74,8 +87,11 @@ input StoryAddInput {
# Set which tags the story should have.
tags: [TagInput!]
# Set the fictional date of the story (RFC3339 format).
fictionalDate: String
# Set the fictional date of the story.
fictionalDate: Time
# Whether to sort by fictional date.
sortByFictionalDate: Boolean
}
# Input for the editStory mutation
@@ -100,8 +116,14 @@ input StoryEditInput {
# Change whether to show this story in the list and search results
listed: Boolean
# Set the fictional date of the story (RFC3339 format).
fictionalDate: String
# Set the fictional date of the story.
fictionalDate: Time
# Clear the fictional date of the story.
clearFictionalDate: Boolean
# Whether to sort by fictional date.
sortByFictionalDate: Boolean
}
# Input for the addStoryTag mutation
@@ -122,20 +144,26 @@ input StoryTagRemoveInput {
tag: TagInput!
}
# Input for the removeStory mutation
input StoryRemoveInput {
# What story to remove.
id: String!
}
# Possible values for Story.category
enum StoryCategory {
# Description and content of a document or item
Document
# General information
Info
# News stories
News
# Description and content of a document or item
Document
# Information about something going on in the background that may or may not inform RP
Background
# General information
Info
# A short story
Story
}
@@ -16,6 +16,15 @@ input TagInput {
name: String!
}
"""
A TagFilter is a filter for the tags query.
"""
input TagFilter {
"Filter by kind"
kind: TagKind
}
# Allowed values for Tag.kind
enum TagKind {
# An organization is a catch all term for in-universe corporations, teams, groups, cults, forces, etc...
@@ -33,4 +42,5 @@ enum TagKind {
# None of the above, but it does still tie multiple stories together. The new story/chapter format may obsolete this tag kind.
Series
}
}
+8
View File
@@ -0,0 +1,8 @@
# Token represents the data parsed and validated from the bearer token.
type Token {
# The user represented in the token.
user: User!
# The permissions for the token.
permissions: [String!]!
}
+20
View File
@@ -0,0 +1,20 @@
"""
An unknown nick is a nick found by the logbot that's not associated with
any characters. Many are going to be false positives, but most should be
useful in pointing out which characters are missing.
"""
type UnknownNick {
"The IRC nick that's unknown."
nick: String!
"The amount of logs that contains the unknown nick."
score: Int!
}
"""
Input for the unknownNicks query.
"""
input UnknownNicksFilter {
"The highest amount of unknown nicks to return."
limit: Int!
}
+8
View File
@@ -0,0 +1,8 @@
# A user represents a member on the RPData platform.
type User {
# The user's unique ID / username
id: String!
# What the user is permitted to do.
permissions: [String!]!
}
+29
View File
@@ -0,0 +1,29 @@
package types
import (
"context"
"log"
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
)
type changeResolver struct{}
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]graphcore.ChangeObject, error) {
objects := obj.Objects()
results := make([]graphcore.ChangeObject, 0, len(objects))
for _, object := range objects {
if changeObject, ok := object.(graphcore.ChangeObject); ok {
results = append(results, changeObject)
} else {
log.Printf("Type %T needs ChangeObject impl", object)
}
}
return results, nil
}
// ChangeResolver is a resolver
var ChangeResolver changeResolver
+35
View File
@@ -0,0 +1,35 @@
package types
import (
"context"
"git.aiterp.net/rpdata/api/services"
"time"
"git.aiterp.net/rpdata/api/models"
)
type chapterResolver struct {
stories *services.StoryService
}
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
if chapter.FictionalDate.IsZero() {
return nil, nil
}
return &chapter.FictionalDate, nil
}
func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]*models.Comment, error) {
if limit == nil {
limit = new(int)
*limit = 0
}
return r.stories.ListComments(ctx, *chapter, *limit)
}
// ChapterResolver is a resolver
func ChapterResolver(s *services.Bundle) *chapterResolver {
return &chapterResolver{stories: s.Stories}
}
+45
View File
@@ -0,0 +1,45 @@
package types
import (
"context"
"git.aiterp.net/rpdata/api/repositories"
"git.aiterp.net/rpdata/api/services"
"github.com/globalsign/mgo"
"time"
"git.aiterp.net/rpdata/api/models"
)
type commentResolver struct {
characters *services.CharacterService
}
func (r *commentResolver) Character(ctx context.Context, obj *models.Comment) (*models.Character, error) {
if obj.CharacterID == "" {
return nil, nil
}
character, err := r.characters.Find(ctx, obj.CharacterID)
if err != nil {
if err == repositories.ErrNotFound || err == mgo.ErrNotFound {
return nil, nil
}
return nil, err
}
return character, nil
}
func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment) (*time.Time, error) {
if obj.FictionalDate.IsZero() {
return nil, nil
}
return &obj.FictionalDate, nil
}
// CommentResolver is a resolver
func CommentResolver(s *services.Bundle) *commentResolver {
return &commentResolver{characters: s.Characters}
}
+45
View File
@@ -0,0 +1,45 @@
package types
import (
"context"
"git.aiterp.net/rpdata/api/services"
"git.aiterp.net/rpdata/api/models"
)
type logResolver struct {
logs *services.LogService
characters *services.CharacterService
channels *services.ChannelService
}
func (r *logResolver) Channel(ctx context.Context, log *models.Log) (*models.Channel, error) {
return r.channels.Find(ctx, log.ChannelName)
}
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]*models.Character, error) {
if len(log.CharacterIDs) == 0 {
return []*models.Character{}, nil
}
return r.characters.List(ctx, models.CharacterFilter{
IDs: log.CharacterIDs,
})
}
func (r *logResolver) Posts(ctx context.Context, log *models.Log, kinds []string) ([]*models.Post, error) {
return r.logs.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
}
func (r *logResolver) NextLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
return r.logs.NextLogs(ctx, obj)
}
func (r *logResolver) PrevLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
return r.logs.PrevLogs(ctx, obj)
}
// LogResolver is a resolver
func LogResolver(s *services.Bundle) *logResolver {
return &logResolver{characters: s.Characters, logs: s.Logs, channels: s.Channels}
}
+30
View File
@@ -0,0 +1,30 @@
package types
import (
"context"
"git.aiterp.net/rpdata/api/services"
"time"
"git.aiterp.net/rpdata/api/models"
)
type storyResolver struct {
stories *services.StoryService
}
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
if story.FictionalDate.IsZero() {
return nil, nil
}
return &story.FictionalDate, nil
}
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]*models.Chapter, error) {
return r.stories.ListChapters(ctx, *story)
}
// StoryResolver is a resolver
func StoryResolver(s *services.Bundle) *storyResolver {
return &storyResolver{stories: s.Stories}
}
+21
View File
@@ -0,0 +1,21 @@
package types
import (
"context"
"git.aiterp.net/rpdata/api/services"
"git.aiterp.net/rpdata/api/models"
)
type tokenResolver struct {
auth *services.AuthService
}
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (*models.User, error) {
return r.auth.FindUser(ctx, token.UserID)
}
// TokenResolver is a resolver
func TokenResolver(s *services.Bundle) *tokenResolver {
return &tokenResolver{auth: s.Auth}
}
+53 -25
View File
@@ -5,55 +5,83 @@ import (
"errors"
"log"
"os"
"strings"
"sync"
"gopkg.in/yaml.v2"
)
var globalMutex sync.Mutex
var global *Config
// Config is configuration
// Config is configuration data
type Config struct {
Space struct {
Host string `json:"host"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
MaxSize int64 `json:"maxSize"`
Root string `json:"root"`
} `json:"space"`
Space Space `json:"space" yaml:"space"`
Database Database `json:"database" yaml:"database"`
Wiki Wiki `json:"wiki" yaml:"wiki"`
}
Database struct {
Host string `json:"host"`
Port int `json:"port"`
Db string `json:"db"`
Username string `json:"username"`
Password string `json:"password"`
Mechanism string `json:"mechanism"`
} `json:"database"`
// Space is configuration for spaces.
type Space struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Host string `json:"host" yaml:"host"`
AccessKey string `json:"accessKey" yaml:"accessKey"`
SecretKey string `json:"secretKey" yaml:"secretKey"`
Bucket string `json:"bucket" yaml:"bucket"`
MaxSize int64 `json:"maxSize" yaml:"maxSize"`
Root string `json:"root" yaml:"root"`
URLRoot string `json:"urlRoot" yaml:"urlRoot"`
}
Wiki struct {
URL string `json:"url"`
} `json:"wiki"`
// Database is configuration for spaces.
type Database struct {
Driver string `json:"driver" yaml:"driver"`
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Db string `json:"db" yaml:"db"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
Mechanism string `json:"mechanism" yaml:"mechanism"`
}
// Wiki is the wiki stuff.
type Wiki struct {
URL string `json:"url" yaml:"url"`
}
// Load loads config stuff
func (config *Config) Load(filename string) error {
log.Println("Trying to load config from " + filename)
stat, err := os.Stat(filename)
if err != nil {
return err
}
if stat.Size() < 1 {
return errors.New("File is empty")
}
file, err := os.Open(filename)
if err != nil {
return err
}
return json.NewDecoder(file).Decode(config)
if strings.HasSuffix(filename, ".json") {
return json.NewDecoder(file).Decode(config)
}
return yaml.NewDecoder(file).Decode(config)
}
// LoadAny loads the first of these files it can find
func (config *Config) LoadAny(filenames ...string) error {
for _, filename := range filenames {
if err := config.Load(filename); err == nil {
return nil
if err := config.Load(filename); err != nil {
continue
}
*config = Config{}
return nil
}
return errors.New("Failed to load configuration files")
@@ -64,7 +92,7 @@ func Global() Config {
globalMutex.Lock()
if global == nil {
global = &Config{}
err := global.LoadAny("/etc/aiterp/rpdata.json", "./config.json")
err := global.LoadAny("/etc/aiterp/rpdata.yaml", "/etc/aiterp/rpdata.json", "./config.yaml", "./config.json")
if err != nil {
log.Fatalln(err)
}
+73
View File
@@ -0,0 +1,73 @@
package generate
import (
"crypto/rand"
"encoding/binary"
mrand "math/rand"
"strconv"
"strings"
)
// ID generates an ID using crypto-random, falling back to math random when that fails
// to avoid disrupting operation because of a faulty RNG.
func ID(prefix string, length int) string {
var data [32]byte
result := strings.Builder{}
result.Grow(length + 32)
result.WriteString(prefix)
pos := 0
for result.Len() < length {
if pos == 0 {
randRead(data[:])
}
result.WriteString(strconv.FormatUint(binary.BigEndian.Uint64(data[pos:pos+8]), 36))
pos = (pos + 8) % 32
}
return result.String()[:length]
}
func randRead(data []byte) {
n, err := rand.Read(data)
if err != nil {
mrand.Read(data[n:])
}
}
// PostID generates a post ID.
func PostID() string {
return ID("P", 16)
}
// StoryID generates a post ID.
func StoryID() string {
return ID("S", 16)
}
// ChapterID generates a post ID.
func ChapterID() string {
return ID("SC", 24)
}
// CommentID generates a post ID.
func CommentID() string {
return ID("SCC", 32)
}
// FileID generates a file ID.
func FileID() string {
return ID("F", 16)
}
// FileID generates a file ID.
func FileUploadID() string {
return ID("U", 24)
}
func KeyID() string {
return ID("K", 32)
}
+20
View File
@@ -0,0 +1,20 @@
package generate
import (
"fmt"
"git.aiterp.net/rpdata/api/models"
"time"
)
// LogID generates a log ID in the format is 2019-05-22_191341547_RedrockAgency
func LogID(log models.Log) string {
if len(log.ChannelName) < 1 {
panic("ChannelName is not valid (validate input before calling this function!)")
}
datetime := log.Date.UTC().Format("2006-01-02_150405")
milliseconds := (log.Date.UnixNano() % int64(time.Second)) / 1000000
channelName := log.ChannelName[1:]
return fmt.Sprintf("%s%03d_%s", datetime, milliseconds, channelName)
}
+346
View File
@@ -0,0 +1,346 @@
package instrumentation
import (
"context"
"reflect"
"sort"
"strings"
"sync"
"time"
"github.com/99designs/gqlgen/graphql"
prometheusclient "github.com/prometheus/client_golang/prometheus"
)
const (
existStatusFailure = "failure"
exitStatusSuccess = "success"
)
var latencyMapMutex sync.Mutex
var latencyMap = make(map[string]float64, 128)
type ctxKeyType string
var ctxKey = ctxKeyType("metricglobal")
var (
requestStartedCounter prometheusclient.Counter
requestCompletedCounter prometheusclient.Counter
requestFailureCounter *prometheusclient.CounterVec
requestSuccessCounter *prometheusclient.CounterVec
requestComplexityHisto prometheusclient.Histogram
resolverStartedCounter *prometheusclient.CounterVec
resolverCompletedCounter *prometheusclient.CounterVec
timeToResolveField *prometheusclient.HistogramVec
timeToHandleRequest *prometheusclient.HistogramVec
timeToHandleRequestQueryMAvg *prometheusclient.GaugeVec
timeToHandleRequestQueryTotal *prometheusclient.CounterVec
responseSizeMAvg *prometheusclient.GaugeVec
responseSizeTotal *prometheusclient.CounterVec
)
// Globals is monitoring data.
type Globals struct {
sync.Mutex
Queries []string
Failed bool
}
// Register registers the prometheus metrics.
func Register() {
requestStartedCounter = prometheusclient.NewCounter(
prometheusclient.CounterOpts{
Name: "graphql_request_started_total",
Help: "Total number of requests started on the graphql server.",
},
)
requestCompletedCounter = prometheusclient.NewCounter(
prometheusclient.CounterOpts{
Name: "graphql_request_completed_total",
Help: "Total number of requests completed on the graphql server.",
},
)
requestFailureCounter = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_request_failures",
Help: "Number of requests with either success or failure status.",
},
[]string{"queries"},
)
requestSuccessCounter = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_request_successes",
Help: "Number of requests with either success or failure status.",
},
[]string{"queries"},
)
requestComplexityHisto = prometheusclient.NewHistogram(
prometheusclient.HistogramOpts{
Name: "graphql_request_complexity",
Help: "The complexity value of incoming queries.",
Buckets: prometheusclient.LinearBuckets(100, 25, 9),
},
)
resolverStartedCounter = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_resolver_started_total",
Help: "Total number of resolver started on the graphql server.",
},
[]string{"object", "field"},
)
resolverCompletedCounter = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_resolver_completed_total",
Help: "Total number of resolver completed on the graphql server.",
},
[]string{"object", "field"},
)
timeToResolveField = prometheusclient.NewHistogramVec(prometheusclient.HistogramOpts{
Name: "graphql_resolver_duration_ms",
Help: "The time taken to resolve a field by graphql server.",
Buckets: prometheusclient.ExponentialBuckets(1, 2, 11),
}, []string{"exitStatus"})
timeToHandleRequest = prometheusclient.NewHistogramVec(prometheusclient.HistogramOpts{
Name: "graphql_request_duration_ms",
Help: "The time taken to handle a request by graphql server.",
Buckets: prometheusclient.ExponentialBuckets(1, 2, 11),
}, []string{"exitStatus"})
timeToHandleRequestQueryMAvg = prometheusclient.NewGaugeVec(
prometheusclient.GaugeOpts{
Name: "graphql_request_query_duration_mavg_ms",
Help: "Moving average of graphql request query duration for a query set.",
},
[]string{"queries"},
)
timeToHandleRequestQueryTotal = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_request_query_duration_total_ms",
Help: "Total duration spent responding to a query.",
},
[]string{"queries"},
)
responseSizeMAvg = prometheusclient.NewGaugeVec(
prometheusclient.GaugeOpts{
Name: "graphql_response_size_mavg_bytes",
Help: "Moving average of graphql response size for a query set.",
},
[]string{"queries"},
)
responseSizeTotal = prometheusclient.NewCounterVec(
prometheusclient.CounterOpts{
Name: "graphql_response_size_total_bytes",
Help: "Total bytes of response data to a query.",
},
[]string{"queries"},
)
prometheusclient.MustRegister(
requestStartedCounter,
requestCompletedCounter,
requestSuccessCounter,
requestFailureCounter,
requestComplexityHisto,
resolverStartedCounter,
resolverCompletedCounter,
timeToResolveField,
timeToHandleRequest,
timeToHandleRequestQueryMAvg,
timeToHandleRequestQueryTotal,
responseSizeMAvg,
responseSizeTotal,
)
}
// UnRegister unregisters
func UnRegister() {
prometheusclient.Unregister(requestStartedCounter)
prometheusclient.Unregister(requestCompletedCounter)
prometheusclient.Unregister(requestSuccessCounter)
prometheusclient.Unregister(requestFailureCounter)
prometheusclient.Unregister(requestComplexityHisto)
prometheusclient.Unregister(resolverStartedCounter)
prometheusclient.Unregister(resolverCompletedCounter)
prometheusclient.Unregister(timeToResolveField)
prometheusclient.Unregister(timeToHandleRequest)
prometheusclient.Unregister(timeToHandleRequestQueryMAvg)
prometheusclient.Unregister(timeToHandleRequestQueryTotal)
prometheusclient.Unregister(responseSizeMAvg)
prometheusclient.Unregister(responseSizeTotal)
}
// ResolverMiddleware is a resolver middleware that logs metrics for prometheus.
func ResolverMiddleware() graphql.FieldMiddleware {
return func(ctx context.Context, next graphql.Resolver) (interface{}, error) {
rctx := graphql.GetResolverContext(ctx)
resolverStartedCounter.WithLabelValues(rctx.Object, rctx.Field.Name).Inc()
observerStart := time.Now()
path := rctx.Path()
if len(path) == 1 {
if globals, ok := ctx.Value(ctxKey).(*Globals); ok {
globals.Lock()
if rctx.Field.Field != nil {
globals.Queries = append(globals.Queries, rctx.Field.Name+"("+argsLabel(rctx.Args)+")")
} else if name, ok := path[0].(string); ok {
globals.Queries = append(globals.Queries, name)
}
globals.Unlock()
}
}
res, err := next(ctx)
var exitStatus string
if err != nil {
exitStatus = existStatusFailure
if globals, ok := ctx.Value(ctxKey).(*Globals); ok {
globals.Lock()
globals.Failed = true
globals.Unlock()
}
} else {
exitStatus = exitStatusSuccess
}
exitStatusLabel := prometheusclient.Labels{"exitStatus": exitStatus}
timeToResolveField.With(exitStatusLabel).Observe(float64(time.Since(observerStart).Nanoseconds() / int64(time.Millisecond)))
resolverCompletedCounter.WithLabelValues(rctx.Object, rctx.Field.Name).Inc()
return res, err
}
}
// RequestMiddleware is a request middleware that logs metrics for prometheus. It looks busy, but the
// overhead is less than 10µs.
func RequestMiddleware() graphql.RequestMiddleware {
return func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
requestStartedCounter.Inc()
// Store some values in a global context.
globals := Globals{}
// Measure the time to serve the request
observerStart := time.Now()
res := next(context.WithValue(ctx, ctxKey, &globals))
observedDuration := time.Since(observerStart)
observedDurationMS := float64(observedDuration.Nanoseconds()) / float64(time.Millisecond)
reqCtx := graphql.GetRequestContext(ctx)
// exitStatus label
exitStatusLabel := prometheusclient.Labels{"exitStatus": exitStatusSuccess}
if globals.Failed {
exitStatusLabel["exitStatus"] = existStatusFailure
}
// queries label
sort.Strings(globals.Queries)
queriesStr := strings.Join(globals.Queries, ";")
queriesLabel := prometheusclient.Labels{
"queries": queriesStr,
}
// Write metrics
timeToHandleRequest.With(exitStatusLabel).Observe(observedDurationMS)
requestCompletedCounter.Inc()
if len(globals.Queries) > 0 {
if globals.Failed {
requestFailureCounter.With(queriesLabel).Inc()
} else {
requestSuccessCounter.With(queriesLabel).Inc()
}
timeToHandleRequestQueryMAvg.With(queriesLabel).Set(movingAvgLatency("ms:"+queriesStr, observedDurationMS))
timeToHandleRequestQueryTotal.With(queriesLabel).Add(observedDurationMS)
responseSizeMAvg.With(queriesLabel).Set(movingAvgLatency("bytes:"+queriesStr, float64(len(res))))
responseSizeTotal.With(queriesLabel).Add(float64(len(res)))
}
requestComplexityHisto.Observe(float64(reqCtx.OperationComplexity))
return res
}
}
func movingAvgLatency(key string, sample float64) float64 {
latencyMapMutex.Lock()
latency, ok := latencyMap[key]
if ok {
latency = (latency * 0.9) + (sample * 0.1)
latencyMap[key] = latency
} else {
latencyMap[key] = sample
latency = sample
}
latencyMapMutex.Unlock()
return latency
}
func argsLabel(args map[string]interface{}) string {
result := ""
for key, value := range args {
if len(result) > 0 {
result += ","
}
result = key
rv := reflect.ValueOf(value)
for rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
continue
}
result += "{"
first := true
for i := 0; i < rv.NumField(); i++ {
rf := rv.Field(i)
if (rf.Kind() == reflect.Ptr || rf.Kind() == reflect.Interface) && rf.IsNil() {
continue
}
rtf := rv.Type().Field(i)
// Empty limits are as good as no limit
if rtf.Name == "Limit" && rf.Type().Kind() == reflect.Int && rf.Int() <= 0 {
continue
}
if !first {
result += ","
}
first = false
jsonTag := rtf.Tag.Get("json")
if jsonTag != "" {
result += jsonTag
} else {
result += strings.ToLower(rtf.Name[0:1]) + rtf.Name[1:]
}
}
result += "}"
}
return result
}
+44
View File
@@ -0,0 +1,44 @@
package notifier
import (
"context"
"sync"
)
// A Notifier is a synchronization primitive for waking upp all listeners at once.
type Notifier struct {
mutex sync.Mutex
ch chan struct{}
}
// Broadcast wakes all listeners if there are any.
func (notifier *Notifier) Broadcast() {
notifier.mutex.Lock()
if notifier.ch != nil {
close(notifier.ch)
notifier.ch = nil
}
notifier.mutex.Unlock()
}
// C gets the channel that'll close on the next notification.
func (notifier *Notifier) C() <-chan struct{} {
notifier.mutex.Lock()
if notifier.ch == nil {
notifier.ch = make(chan struct{})
}
ch := notifier.ch
notifier.mutex.Unlock()
return ch
}
// Wait waits for the next `Broadcast` call, or the context's termination.
func (notifier *Notifier) Wait(ctx context.Context) error {
select {
case <-notifier.C():
return nil
case <-ctx.Done():
return ctx.Err()
}
}
-20
View File
@@ -1,20 +0,0 @@
package session
import "context"
type contextKeyType struct{ name string }
func (ck *contextKeyType) String() string {
return ck.name
}
var contextKey = &contextKeyType{name: "session context key"}
// FromContext gets a session fron the context.
func FromContext(ctx context.Context) *Session {
return ctx.Value(contextKey).(*Session)
}
func contextWithSession(parent context.Context, session *Session) context.Context {
return context.WithValue(parent, contextKey, session)
}
-39
View File
@@ -1,39 +0,0 @@
package session
// DefaultPermissions gets the default permissions
func DefaultPermissions() []string {
return []string{
"member",
"log.edit",
"log.reorder",
"post.edit",
"post.move",
"file.upload",
}
}
// AllPermissions gets all permissions and their purpose
func AllPermissions() map[string]string {
return map[string]string{
"member": "Can add/edit/remove own content",
"user.edit": "Can edit non-owned users",
"character.add": "Can add non-owned characters",
"character.edit": "Can edit non-owned characters",
"character.remove": "Can remove non-owned characters",
"channel.add": "Can add channels",
"channel.edit": "Can edit channels",
"channel.remove": "Can remove channels",
"log.add": "Can add logs",
"log.edit": "Can edit logs",
"log.remove": "Can remove logs",
"post.add": "Can add posts",
"post.edit": "Can edit posts",
"post.move": "Can move posts",
"post.remove": "Can remove posts",
"story.edit": "Can edit non-owned stories",
"story.remove": "Can remove non-owned stories",
"file.upload": "Can upload files",
"file.edit": "Can edit non-owned files",
"file.remove": "Can remove non-owned files",
}
}
-182
View File
@@ -1,182 +0,0 @@
package session
import (
"crypto/rand"
"encoding/hex"
"log"
"net/http"
"strings"
"sync"
"time"
"git.aiterp.net/aiterp/wikiauth"
"git.aiterp.net/rpdata/api/internal/config"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var sessionCollection *mgo.Collection
// A Session represents a login session.
type Session struct {
mutex sync.Mutex
ID string `bson:"_id"`
Time time.Time `bson:"time"`
UserID string `bson:"userId"`
user *User
w http.ResponseWriter
}
// Load loads a session from a cookie, returning either `r` or a request
// with the session context.
func Load(w http.ResponseWriter, r *http.Request) *http.Request {
cookie, err := r.Cookie("aiterp_session")
if err != nil {
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
}
id := cookie.Value
session := Session{}
err = sessionCollection.FindId(id).One(&session)
if err != nil || time.Since(session.Time) > time.Hour*168 {
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
}
if session.ID != "" && time.Since(session.Time) > time.Second*30 {
session.Time = time.Now()
go sessionCollection.UpdateId(id, bson.M{"$set": bson.M{"time": session.Time}})
}
cookie.Expires = time.Now().Add(time.Hour * 168)
http.SetCookie(w, cookie)
session.w = w
return r.WithContext(contextWithSession(r.Context(), &session))
}
// Login logs a user in.
func (session *Session) Login(username, password string) error {
auth := wikiauth.New(config.Global().Wiki.URL)
err := auth.Login(username, password)
if err != nil {
return err
}
// Allow bot passwords
username = strings.SplitN(username, "@", 2)[0]
data := make([]byte, 32)
_, err = rand.Read(data)
if err != nil {
return err
}
session.ID = hex.EncodeToString(data)
session.UserID = username
session.Time = time.Now()
err = sessionCollection.Insert(&session)
if err != nil {
return err
}
http.SetCookie(session.w, &http.Cookie{
Name: "aiterp_session",
Value: session.ID,
Expires: time.Now().Add(time.Hour * 2160), // 90 days
HttpOnly: true,
})
user, err := FindUser(session.UserID)
if err == mgo.ErrNotFound {
user = User{ID: username, Nick: "", Permissions: DefaultPermissions()}
err := userCollection.Insert(user)
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
// Logout logs out the session
func (session *Session) Logout() {
http.SetCookie(session.w, &http.Cookie{
Name: "aiterp_session",
Value: "",
Expires: time.Unix(0, 0),
HttpOnly: true,
})
session.mutex.Lock()
session.user = nil
session.UserID = ""
session.ID = ""
session.mutex.Unlock()
sessionCollection.RemoveId(session.ID)
}
// User gets the user information for the session.
func (session *Session) User() *User {
session.mutex.Lock()
defer session.mutex.Unlock()
if session.user != nil {
return session.user
}
if session.UserID == "" {
return nil
}
user, err := FindUser(session.UserID)
if err != nil {
return nil
}
return &user
}
// NameOrPermitted is a shorthand for checking the username OR permissions, e.g. to check
// if a logged in user can edit a certain post.
func (session *Session) NameOrPermitted(userid string, permissions ...string) bool {
if session.UserID == userid {
return true
}
user := session.User()
if user == nil {
return false
}
return user.Permitted()
}
func init() {
store.HandleInit(func(db *mgo.Database) {
sessionCollection = db.C("core.sessions")
sessionCollection.EnsureIndexKey("nick")
sessionCollection.EnsureIndexKey("userId")
err := sessionCollection.EnsureIndex(mgo.Index{
Name: "time",
Key: []string{"time"},
ExpireAfter: time.Hour * 168,
})
if err != nil {
log.Fatalln(err)
}
})
}
-73
View File
@@ -1,73 +0,0 @@
package store
import (
"fmt"
"time"
"github.com/globalsign/mgo"
)
var db *mgo.Database
var dbInits []func(db *mgo.Database)
// ConnectDB connects to a mongodb database.
func ConnectDB(host string, port int, database, username, password, mechanism string) error {
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{fmt.Sprintf("%s:%d", host, port)},
Timeout: 30 * time.Second,
Database: database,
Username: username,
Password: password,
Mechanism: mechanism,
Source: database,
})
if err != nil {
return err
}
db = session.DB(database)
return setupDB()
}
// HandleInit handles the initialization of the database
func HandleInit(function func(db *mgo.Database)) {
dbInits = append(dbInits, function)
}
func setupDB() error {
db.C("common.characters").EnsureIndexKey("name")
db.C("common.characters").EnsureIndexKey("shortName")
db.C("common.characters").EnsureIndexKey("author")
err := db.C("common.characters").EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
return err
}
db.C("logbot3.logs").EnsureIndexKey("date")
db.C("logbot3.logs").EnsureIndexKey("channel")
db.C("logbot3.logs").EnsureIndexKey("channel", "open")
db.C("logbot3.logs").EnsureIndexKey("open")
db.C("logbot3.logs").EnsureIndexKey("oldId")
db.C("logbot3.logs").EnsureIndexKey("characterIds")
db.C("logbot3.logs").EnsureIndexKey("event")
db.C("logbot3.logs").EnsureIndexKey("$text:channel", "$text:title", "$text:event", "$text:description", "$text:posts.nick", "$text:posts.text")
err = db.C("server.changes").EnsureIndex(mgo.Index{
Key: []string{"date"},
ExpireAfter: time.Hour * (24 * 14),
})
if err != nil {
return err
}
for _, dbInit := range dbInits {
dbInit(db)
}
return nil
}
-37
View File
@@ -1,37 +0,0 @@
package store
import (
"sync"
"git.aiterp.net/rpdata/api/internal/config"
)
var initMuted sync.Mutex
var hasInitialized bool
// Init initalizes the store
func Init() error {
initMuted.Lock()
defer initMuted.Unlock()
if hasInitialized {
return nil
}
conf := config.Global()
dbconf := conf.Database
err := ConnectDB(dbconf.Host, dbconf.Port, dbconf.Db, dbconf.Username, dbconf.Password, dbconf.Mechanism)
if err != nil {
return err
}
sconf := conf.Space
err = ConnectSpace(sconf.Host, sconf.AccessKey, sconf.SecretKey, sconf.Bucket, sconf.MaxSize, sconf.Root)
if err != nil {
return err
}
hasInitialized = true
return nil
}
-85
View File
@@ -1,85 +0,0 @@
package store
import (
"context"
"errors"
"fmt"
"io"
minio "github.com/minio/minio-go"
)
var spaceBucket string
var spaceURLRoot string
var spaceRoot string
var spaceClient *minio.Client
var spaceMaxSize int64
// ConnectSpace connects to a S3 space.
func ConnectSpace(host, accessKey, secretKey, bucket string, maxSize int64, rootDirectory string) error {
client, err := minio.New(host, accessKey, secretKey, true)
if err != nil {
return err
}
exists, err := client.BucketExists(bucket)
if err != nil {
return err
}
if !exists {
return errors.New("Bucket not found")
}
spaceClient = client
spaceBucket = bucket
spaceURLRoot = fmt.Sprintf("https://%s.%s/%s/", bucket, host, rootDirectory)
spaceMaxSize = maxSize
spaceRoot = rootDirectory
return nil
}
// UploadFile uploads the file to the space. This does not do any checks on it, so the endpoints should
// ensure that's all okay.
func UploadFile(ctx context.Context, folder string, name string, mimeType string, reader io.Reader, size int64) (string, error) {
path := folder + "/" + name
if size > spaceMaxSize {
return "", errors.New("File is too big")
}
_, err := spaceClient.PutObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, reader, size, minio.PutObjectOptions{
ContentType: mimeType,
UserMetadata: map[string]string{
"x-amz-acl": "public-read",
},
})
if err != nil {
return "", err
}
_, err = spaceClient.StatObject(spaceBucket, spaceRoot+"/"+path, minio.StatObjectOptions{})
if err != nil {
return "", err
}
return path, nil
}
// RemoveFile removes a file from the space
func RemoveFile(folder string, name string) error {
path := folder + "/" + name
return spaceClient.RemoveObject(spaceBucket, spaceRoot+"/"+path)
}
// DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it!
func DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) {
return spaceClient.GetObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, minio.GetObjectOptions{})
}
// URLFromPath gets the URL from the path returned by UploadFile
func URLFromPath(path string) string {
return spaceURLRoot + path
}
+84
View File
@@ -0,0 +1,84 @@
package task
import (
"context"
"sync"
"time"
)
var globalCtx, globalCancel = context.WithCancel(context.Background())
// A Task is a wrapper around a function that offers scheduling
// and syncronization.
type Task struct {
mutex sync.Mutex
ctx context.Context
ctxCancel context.CancelFunc
waitDuration time.Duration
scheduled bool
callback func() error
}
// Context gets the task's context.
func (task *Task) Context() context.Context {
return task.ctx
}
// Stop stops a task. This should not be done to stop a single run,
// but as a way to cancel it forever.
func (task *Task) Stop() {
task.ctxCancel()
}
// Schedule schedules a task for later execution.
func (task *Task) Schedule() {
// Don't if the context is closed.
select {
case <-task.Context().Done():
return
default:
}
task.mutex.Lock()
if task.scheduled {
task.mutex.Unlock()
return
}
task.scheduled = true
task.mutex.Unlock()
go func() {
<-time.After(task.waitDuration)
// Mark as no longer scheduled
task.mutex.Lock()
task.scheduled = false
task.mutex.Unlock()
// Don't if the task is cancelled
select {
case <-task.Context().Done():
return
default:
}
task.callback()
}()
}
// New makes a new task with the callback.
func New(waitDuration time.Duration, callback func() error) *Task {
ctx, ctxCancel := context.WithCancel(globalCtx)
return &Task{
callback: callback,
ctx: ctx,
ctxCancel: ctxCancel,
waitDuration: waitDuration,
}
}
// StopAll stops all tasks.
func StopAll() {
globalCancel()
}
-66
View File
@@ -1,66 +0,0 @@
package loader
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/model/channel"
"github.com/graph-gophers/dataloader"
)
// Channel gets a character by key
func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
if !strings.HasPrefix(key, "Channel.") {
key = "Channel." + key
}
if loader.loaders[key] == nil {
return channel.Channel{}, errors.New("unsupported key")
}
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return channel.Channel{}, err
}
channel, ok := res.(channel.Channel)
if !ok {
return channel, errors.New("incorrect type")
}
return channel, nil
}
func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
var results []*dataloader.Result
names := keys.Keys()
channels, err := channel.ListNames(names...)
if err != nil {
for range names {
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
}
return results
}
for _, name := range names {
found := false
for i := range channels {
if channels[i].Name == name {
results = append(results, &dataloader.Result{Data: channels[i]})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
}
}
return results
}
-130
View File
@@ -1,130 +0,0 @@
package loader
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/model/character"
"github.com/graph-gophers/dataloader"
)
// Character gets a character by key
func (loader *Loader) Character(key, value string) (character.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
if loader.loaders[key] == nil {
return character.Character{}, errors.New("unsupported key")
}
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
res, err := thunk()
if err != nil {
return character.Character{}, err
}
char, ok := res.(character.Character)
if !ok {
return character.Character{}, errors.New("incorrect type")
}
return char, nil
}
// Characters gets characters by key
func (loader *Loader) Characters(key string, values ...string) ([]character.Character, error) {
if !strings.HasPrefix(key, "Character.") {
key = "Character." + key
}
if loader.loaders[key] == nil {
return nil, errors.New("unsupported key")
}
thunk := loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(values))
res, errs := thunk()
for _, err := range errs {
if err != nil && err != ErrNotFound {
return nil, err
}
}
chars := make([]character.Character, len(res))
for i := range res {
char, ok := res[i].(character.Character)
if !ok {
return nil, errors.New("incorrect type")
}
chars[i] = char
}
return chars, nil
}
func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
results := make([]*dataloader.Result, 0, len(keys))
ids := keys.Keys()
characters, err := character.ListIDs(ids...)
if err != nil {
for range ids {
results = append(results, &dataloader.Result{Error: err})
}
return results
}
for _, id := range ids {
found := false
for _, character := range characters {
if character.ID == id {
results = append(results, &dataloader.Result{Data: character})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: character.Character{}, Error: ErrNotFound})
}
}
return results
}
func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
var results []*dataloader.Result
nicks := keys.Keys()
characters, err := character.ListNicks(nicks...)
if err != nil {
for range nicks {
results = append(results, &dataloader.Result{Error: err})
}
return results
}
for _, nick := range nicks {
found := false
for i := range characters {
if characters[i].HasNick(nick) {
results = append(results, &dataloader.Result{Data: characters[i]})
found = true
break
}
}
if !found {
results = append(results, &dataloader.Result{Data: character.Character{}, Error: err})
}
}
return results
}
-49
View File
@@ -1,49 +0,0 @@
package loader
import (
"context"
"errors"
"time"
"github.com/graph-gophers/dataloader"
)
var contextKey struct{}
// ErrNotFound is returned in batches when one or more things weren't found. Usually harmless.
var ErrNotFound = errors.New("not found")
// A Loader is a collection of data loaders and functions to act on them. It's supposed to be
// request-scoped, and will thus keep things cached indefinitely.
type Loader struct {
ctx context.Context
loaders map[string]*dataloader.Loader
}
// New initializes the loader.
func New() *Loader {
return &Loader{
ctx: context.Background(),
loaders: map[string]*dataloader.Loader{
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond)),
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond)),
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond)),
},
}
}
// FromContext gets the Loader from context.
func FromContext(ctx context.Context) *Loader {
value := ctx.Value(&contextKey)
if value == nil {
return nil
}
return value.(*Loader)
}
// ToContext gets a context with the loader as a value
func (loader *Loader) ToContext(ctx context.Context) context.Context {
loader.ctx = ctx
return context.WithValue(ctx, &contextKey, loader)
}
-105
View File
@@ -1,105 +0,0 @@
package change
import (
"log"
"sync"
"time"
"git.aiterp.net/rpdata/api/model/counter"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
)
var collection *mgo.Collection
// A Change represents a change in any other model
type Change struct {
ID int `bson:"_id" json:"id"`
Time time.Time `bson:"time" json:"time"`
Model string `bson:"model" json:"model"`
Op string `bson:"op" json:"op"`
Author string `bson:"author,omitempty" json:"author,omitempty"`
ObjectID string `bson:"objectId,omitempty" json:"objectId,omitempty"`
Data interface{} `bson:"data,omitempty" json:"data,omitempty"`
}
// PublicModels lists which models can be listed in bulk by anyone.
var PublicModels = []string{
"Character",
"Log",
"Post",
}
var submitMutex sync.Mutex
// Submit submits a change to the history.
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
submitMutex.Lock()
defer submitMutex.Unlock()
index, err := counter.Next("auto_increment", "Change")
if err != nil {
return Change{}, err
}
change := Change{
ID: index,
Time: time.Now(),
Model: model,
Op: op,
Author: author,
ObjectID: objectID,
Data: data,
}
Timeline.push(change)
err = collection.Insert(&change)
if err != nil {
return Change{}, err
}
return change, err
}
// FindID gets a change by ID
func FindID(id int) (Change, error) {
change := Change{}
err := collection.FindId(id).One(&change)
if err != nil {
return Change{}, err
}
return change, nil
}
// List gets all changes in ascending order
func List() ([]Change, error) {
changes := make([]Change, 0, 64)
err := collection.Find(nil).Sort("_id").All(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.history")
collection.EnsureIndexKey("model")
collection.EnsureIndexKey("author")
collection.EnsureIndexKey("objectId")
err := collection.EnsureIndex(mgo.Index{
Name: "expiry",
Key: []string{"time"},
ExpireAfter: time.Hour * 336,
})
if err != nil {
log.Fatalln(err)
}
})
}
-118
View File
@@ -1,118 +0,0 @@
package change
import (
"context"
"errors"
"sync"
"github.com/globalsign/mgo"
)
// ErrInvalidIndex is returned when a timeline subscriber tries to get a value
// with a negative id
var ErrInvalidIndex = errors.New("change.timeline: Invalid ID")
// ErrWaitRequired is returned when a timeline subscriber requests an entry that
// has not yet been pushed
var ErrWaitRequired = errors.New("change.timeline: Waiting required")
// ErrExpired is returned when a timeline subscriber requests a change that happened
// too long ago for it to be in the database.
var ErrExpired = errors.New("change.timeline: Change has expired")
type timeline struct {
mutex sync.Mutex
firstIndex int64
currentID int64
cache []Change
notifyCh chan struct{}
}
func (timeline *timeline) Get(ctx context.Context, id int64) (Change, error) {
change, err := timeline.get(id, false)
if err == ErrWaitRequired {
if err := timeline.wait(ctx); err != nil {
return Change{}, err
}
return timeline.get(id, true)
} else if err == mgo.ErrNotFound {
return Change{}, ErrExpired
}
return change, nil
}
func (timeline *timeline) push(change Change) {
timeline.mutex.Lock()
timeline.cache = append(timeline.cache, change)
if len(timeline.cache) > 128 {
copy(timeline.cache, timeline.cache[32:])
timeline.cache = timeline.cache[:len(timeline.cache)-32]
}
// Only update the currentID if this is more recent than the existing ones
if int64(change.ID) > timeline.currentID {
timeline.currentID = int64(change.ID)
}
if timeline.notifyCh != nil {
close(timeline.notifyCh)
timeline.notifyCh = nil
}
timeline.mutex.Unlock()
}
func (timeline *timeline) get(id int64, hasWaited bool) (Change, error) {
if id < 0 {
return Change{}, ErrInvalidIndex
}
timeline.mutex.Lock()
if !hasWaited && id > timeline.currentID {
timeline.mutex.Unlock()
return Change{}, ErrWaitRequired
}
for _, change := range timeline.cache {
if change.ID == int(id) {
timeline.mutex.Unlock()
return change, nil
}
}
timeline.mutex.Unlock()
return FindID(int(id))
}
func (timeline *timeline) wait(ctx context.Context) error {
timeline.mutex.Lock()
if timeline.notifyCh == nil {
timeline.notifyCh = make(chan struct{})
}
ch := timeline.notifyCh
timeline.mutex.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
}
// Timeline has operations for subscribing to changes in real time
var Timeline = &timeline{}
// InitializeTimeline fills in the timeline with existing entries
func InitializeTimeline() {
changes, err := List()
if err != nil {
return
}
for _, change := range changes {
Timeline.push(change)
}
}
-143
View File
@@ -1,143 +0,0 @@
package channel
import (
"errors"
"strings"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var collection *mgo.Collection
// ErrInvalidName is an error for an invalid channel name
var ErrInvalidName = errors.New("Invalid channel name")
// A Channel represents information abount an IRC RP channel, and whether it should be logged
type Channel struct {
Name string `bson:"_id"`
Logged bool `bson:"logged"`
Hub bool `bson:"hub"`
Event string `bson:"event,omitempty"`
Location string `bson:"location,omitempty"`
}
// Edit edits the channel
func (channel *Channel) Edit(logged, hub *bool, event, location *string) error {
changes := bson.M{}
changed := *channel
if logged != nil && channel.Logged != *logged {
changes["logged"] = *logged
changed.Logged = *logged
}
if hub != nil && channel.Hub != *hub {
changes["hub"] = *hub
changed.Hub = *hub
}
if event != nil && channel.Event != *event {
changes["event"] = *event
changed.Event = *event
}
if location != nil && channel.Event != *location {
changes["location"] = *location
changed.Location = *location
}
if len(changes) == 0 {
return nil
}
err := collection.UpdateId(channel.Name, bson.M{"$set": changes})
if err != nil {
return err
}
*channel = changed
return nil
}
// Remove removes the channel information from the database.
func (channel *Channel) Remove() error {
return collection.RemoveId(channel.Name)
}
// Ensure ensures a channel's existence. It does not change `logged` if there is
// an existing channel.
func Ensure(name string, logged bool) (Channel, error) {
channel, err := FindName(name)
if err == mgo.ErrNotFound {
return New(name, logged, false, "", "")
} else if err != nil {
return Channel{}, err
}
return channel, nil
}
// New creates a new channel
func New(name string, logged, hub bool, event, location string) (Channel, error) {
if len(name) < 3 && !strings.HasPrefix(name, "#") {
return Channel{}, ErrInvalidName
}
channel := Channel{
Name: name,
Logged: logged,
Hub: hub,
Event: event,
Location: location,
}
err := collection.Insert(channel)
if err != nil {
return Channel{}, err
}
return channel, nil
}
// FindName finds a channel by its id (its name).
func FindName(name string) (Channel, error) {
channel := Channel{}
err := collection.FindId(name).One(&channel)
return channel, err
}
// List finds channels, if logged is true it will be limited to logged
// channels
func List(logged bool) ([]Channel, error) {
query := bson.M{}
if logged {
query["logged"] = true
}
channels := make([]Channel, 0, 32)
err := collection.Find(query).All(&channels)
return channels, err
}
// ListNames finds channels by the names provided
func ListNames(names ...string) ([]Channel, error) {
query := bson.M{"_id": bson.M{"$in": names}}
channels := make([]Channel, 0, 32)
err := collection.Find(query).All(&channels)
return channels, err
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.channels")
collection.EnsureIndexKey("logged")
collection.EnsureIndexKey("hub")
collection.EnsureIndexKey("event")
collection.EnsureIndexKey("location")
})
}
-231
View File
@@ -1,231 +0,0 @@
package character
import (
"errors"
"log"
"strconv"
"strings"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/counter"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var collection *mgo.Collection
// Character is a common data model representing an RP character or NPC.
type Character struct {
ID string `json:"id" bson:"_id"`
Nicks []string `json:"nicks" bson:"nicks"`
Name string `json:"name" bson:"name"`
ShortName string `json:"shortName" bson:"shortName"`
Author string `json:"author" bson:"author"`
Description string `json:"description" bson:"description"`
}
// HasNick returns true if the character has that nick
func (character *Character) HasNick(nick string) bool {
for i := range character.Nicks {
if strings.EqualFold(character.Nicks[i], nick) {
return true
}
}
return false
}
// AddNick adds a nick to the character. It will return an error
// if the nick already exists.
func (character *Character) AddNick(nick string) error {
for i := range character.Nicks {
if strings.EqualFold(character.Nicks[i], nick) {
return errors.New("Nick already exists")
}
}
err := collection.UpdateId(character.ID, bson.M{"$push": bson.M{"nicks": nick}})
if err != nil {
return err
}
character.Nicks = append(character.Nicks, nick)
return nil
}
// RemoveNick removes the nick from the character. It will raise
// an error if the nick does not exist; even if that kind of is
// the end goal.
func (character *Character) RemoveNick(nick string) error {
index := -1
for i := range character.Nicks {
if strings.EqualFold(character.Nicks[i], nick) {
index = i
break
}
}
if index == -1 {
return errors.New("Nick does not exist")
}
err := collection.UpdateId(character.ID, bson.M{"$pull": bson.M{"nicks": nick}})
if err != nil {
return err
}
character.Nicks = append(character.Nicks[:index], character.Nicks[index+1:]...)
return nil
}
// Edit sets the fields of metadata. Only non-empty and different fields will be set in the
// database, preventing out of order edits to two fields from conflicting
func (character *Character) Edit(name, shortName, description string) error {
changes := bson.M{}
if len(name) > 0 && name != character.Name {
changes["name"] = name
}
if len(shortName) > 0 && shortName != character.ShortName {
changes["shortName"] = shortName
}
if len(description) > 0 && description != character.Description {
changes["description"] = description
}
err := collection.UpdateId(character.ID, changes)
if err != nil {
return err
}
if changes["name"] != nil {
character.Name = name
}
if changes["shortName"] != nil {
character.ShortName = shortName
}
if changes["description"] != nil {
character.Description = description
}
return nil
}
// Remove removes the character from the database. The reason this is an instance method
// is that it should only be done after an authorization check.
func (character *Character) Remove() error {
return collection.RemoveId(character.ID)
}
// FindID finds Character by ID
func FindID(id string) (Character, error) {
return find(bson.M{"_id": id})
}
// FindNick finds Character by nick
func FindNick(nick string) (Character, error) {
return find(bson.M{"nicks": nick})
}
// FindName finds Character by either full name or
// short name.
func FindName(name string) (Character, error) {
return find(bson.M{"$or": []bson.M{bson.M{"name": name}, bson.M{"shortName": name}}})
}
// List lists all characters
func List() ([]Character, error) {
return list(bson.M{})
}
// ListAuthor lists all characters by author
func ListAuthor(author string) ([]Character, error) {
return list(bson.M{"author": author})
}
// ListNicks lists all characters with either of these nicks. This was made with
// the logbot in mind, to batch an order for characters.
func ListNicks(nicks ...string) ([]Character, error) {
return list(bson.M{"nicks": bson.M{"$in": nicks}})
}
// ListIDs lists all characters with either of these IDs.
func ListIDs(ids ...string) ([]Character, error) {
return list(bson.M{"_id": bson.M{"$in": ids}})
}
// New creates a Character and pushes it to the database. It does some validation
// on nick, name, shortName and author. Leave the shortname blank to have it be the
// first name.
func New(nick, name, shortName, author, description string) (Character, error) {
if len(nick) < 1 || len(name) < 1 || len(author) < 1 {
return Character{}, errors.New("Nick, name, or author name too short or empty")
}
if shortName == "" {
shortName = strings.SplitN(name, " ", 2)[0]
}
char, err := FindNick(nick)
if err == nil && char.ID != "" {
return Character{}, errors.New("Nick is occupied")
}
nextID, err := counter.Next("auto_increment", "Character")
if err != nil {
return Character{}, err
}
character := Character{
ID: "C" + strconv.Itoa(nextID),
Nicks: []string{nick},
Name: name,
ShortName: shortName,
Author: author,
Description: description,
}
err = collection.Insert(character)
if err != nil {
return Character{}, err
}
return character, nil
}
func find(query interface{}) (Character, error) {
character := Character{}
err := collection.Find(query).One(&character)
if err != nil {
return Character{}, err
}
return character, nil
}
func list(query interface{}) ([]Character, error) {
characters := make([]Character, 0, 64)
err := collection.Find(query).All(&characters)
if err != nil {
return nil, err
}
return characters, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.characters")
collection.EnsureIndexKey("name")
collection.EnsureIndexKey("shortName")
collection.EnsureIndexKey("author")
err := collection.EnsureIndex(mgo.Index{
Key: []string{"nicks"},
Unique: true,
DropDups: true,
})
if err != nil {
log.Fatalln("init common.characters:", err)
}
})
}
-37
View File
@@ -1,37 +0,0 @@
package counter
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var collection *mgo.Collection
type counter struct {
ID string `bson:"_id"`
Value int `bson:"value"`
}
// Next gets the next value of a counter, or an error if it hasn't.
func Next(category, name string) (int, error) {
id := category + "." + name
doc := counter{}
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
Update: bson.M{"$inc": bson.M{"value": 1}},
Upsert: true,
ReturnNew: true,
}, &doc)
if err != nil {
return -1, err
}
return doc.Value, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("core.counters")
})
}
-252
View File
@@ -1,252 +0,0 @@
package file
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"io"
"strconv"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var fileCollection *mgo.Collection
// A File is a record of a file stored in the Space.
type File struct {
ID string `bson:"_id" json:"id"`
Time time.Time `bson:"time" json:"time"`
Kind string `bson:"kind" json:"kind"`
Public bool `bson:"public" json:"public"`
Name string `bson:"name" json:"name"`
MimeType string `bson:"mimeType" json:"mimeType"`
Size int64 `bson:"size" json:"size"`
Author string `bson:"author" json:"author"`
URL string `bson:"url,omitempty" json:"url,omitempty"`
}
// Edit edits the file, changing up to both the two mutable properties
func (file *File) Edit(name *string, public *bool) error {
changes := bson.M{}
changedFile := *file
if name != nil && *name != file.Name {
changes["name"] = *name
changedFile.Name = *name
}
if public != nil && *public != file.Public {
changes["public"] = *public
changedFile.Public = *public
}
if len(changes) == 0 {
return nil
}
err := fileCollection.UpdateId(file.ID, bson.M{"$set": changes})
if err != nil {
return err
}
*file = changedFile
return nil
}
// Delete removes the file information from the database, and deletes the file.
func (file *File) Delete() error {
err := fileCollection.RemoveId(file.ID)
if err != nil {
return err
}
if file.Kind == "upload" {
err = store.RemoveFile("files", file.ID)
if err != nil {
return err
}
}
file.URL = ""
return nil
}
// Insert manually inserts file information into the database. This should never, ever be API accessible
func Insert(name, kind, mimeType, author string, time time.Time, size int64, url string) (File, error) {
file := File{
ID: makeFileID(),
Kind: kind,
Time: time,
Public: false,
Author: author,
Name: name,
MimeType: mimeType,
Size: size,
URL: url,
}
err := fileCollection.Insert(file)
if err != nil {
return File{}, err
}
return file, nil
}
// Upload adds a file to the space.
func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (File, error) {
if !allowdMimeTypes[mimeType] {
return File{}, errors.New("File type not allowed:" + mimeType)
}
if name == "" {
date := time.Now().UTC().Format("Jan 02 2006 15:04:05 MST")
name = "Unnamed file (" + date + ")"
}
if mimeType == "" {
mimeType = "binary/octet-stream"
}
id := makeFileID()
path, err := store.UploadFile(ctx, "files", id, mimeType, input, size)
if err != nil {
return File{}, err
}
file := File{
ID: id,
Kind: "upload",
Time: time.Now(),
Public: false,
Author: author,
Name: name,
MimeType: mimeType,
Size: size,
URL: store.URLFromPath(path),
}
err = fileCollection.Insert(file)
if err != nil {
return File{}, err
}
return file, nil
}
// FindID finds a file by ID
func FindID(id string) (File, error) {
file := File{}
err := fileCollection.FindId(id).One(&file)
if err != nil {
return File{}, err
}
return file, nil
}
// FindName finds a file by kind, name and author
func FindName(kind string, name string, author string) (File, error) {
query := bson.M{"kind": kind, "name": name, "author": author}
file := File{}
err := fileCollection.Find(query).One(&file)
if err != nil {
return File{}, err
}
return file, nil
}
// List lists files according to the standard lookup. By default it's just the author's own files,
// but if `public` is true it will alos include files made public by other authors. If `mimeTypes` contains
// any, it will limit the results to that. If `author` is empty, it will only list public files
func List(author string, public bool, mimeTypes []string) ([]File, error) {
query := bson.M{}
if author != "" {
if public {
query["$or"] = []bson.M{
bson.M{"author": author},
bson.M{"public": true},
}
} else {
query["author"] = author
}
} else {
if !public {
return nil, errors.New("No author specified, and public is unset")
}
query["public"] = true
}
if len(mimeTypes) > 0 {
query["mimeTypes"] = bson.M{"$in": mimeTypes}
}
return listFiles(query)
}
func listFiles(query interface{}) ([]File, error) {
list := make([]File, 0, 32)
err := fileCollection.Find(query).Sort("-time").All(&list)
if err != nil {
return nil, err
}
return list, nil
}
// makeFileID makes a random file ID that's 32 characters long
func makeFileID() string {
result := "F" + strconv.FormatInt(time.Now().UnixNano(), 36)
offset := 0
data := make([]byte, 32)
rand.Read(data)
for len(result) < 32 {
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
offset += 8
if offset >= 32 {
rand.Read(data)
offset = 0
}
}
return result[:32]
}
func init() {
store.HandleInit(func(db *mgo.Database) {
fileCollection = db.C("file.headers")
fileCollection.EnsureIndexKey("author")
fileCollection.EnsureIndexKey("public")
fileCollection.EnsureIndexKey("kind", "name", "author")
fileCollection.EnsureIndexKey("author", "public")
fileCollection.EnsureIndexKey("kind")
})
}
var allowdMimeTypes = map[string]bool{
"": false,
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"text/plain": true,
"application/json": true,
"application/pdf": false,
"binary/octet-stream": false,
"video/mp4": false,
"audio/mp3": false,
}
-388
View File
@@ -1,388 +0,0 @@
package log
import (
"errors"
"fmt"
"log"
"sort"
"strconv"
"strings"
"sync"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/model/channel"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/model/counter"
"github.com/globalsign/mgo/bson"
"github.com/globalsign/mgo"
)
var postMutex sync.RWMutex
var characterUpdateMutex sync.Mutex
var logsCollection *mgo.Collection
// Log is the header/session for a log file.
type Log struct {
ID string `bson:"_id"`
ShortID string `bson:"shortId"`
Date time.Time `bson:"date"`
Channel string `bson:"channel"`
Title string `bson:"title,omitempty"`
Event string `bson:"event,omitempty"`
Description string `bson:"description,omitempty"`
Open bool `bson:"open"`
CharacterIDs []string `bson:"characterIds"`
}
// New creates a new Log
func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) {
nextID, err := counter.Next("auto_increment", "Log")
if err != nil {
return Log{}, err
}
_, err = channel.Ensure(channelName, open)
if err != nil {
return Log{}, err
}
log := Log{
ID: MakeLogID(date, channelName),
ShortID: "L" + strconv.Itoa(nextID),
Date: date,
Channel: channelName,
Title: title,
Event: event,
Description: description,
Open: open,
CharacterIDs: nil,
}
err = logsCollection.Insert(log)
if err != nil {
return Log{}, err
}
return log, nil
}
// FindID finds a log either by it's ID or short ID.
func FindID(id string) (Log, error) {
return findLog(bson.M{
"$or": []bson.M{
bson.M{"_id": id},
bson.M{"shortId": id},
},
})
}
// List lists all logs
func List(limit int) ([]Log, error) {
return listLog(bson.M{}, limit)
}
// ListSearch lists the logs matching the parameters. Empty/zero values means the parameter is ingored when
// building the query. This is the old aitelogs2 way, but with the addition of a text search.
//
// If a text search is specified, it will make two trips to the database.
func ListSearch(textSearch string, channels []string, characterIds []string, events []string, open bool, limit int) ([]Log, error) {
postMutex.RLock()
defer postMutex.RUnlock()
query := bson.M{}
// Run a text search
if textSearch != "" {
searchResults := make([]string, 0, 32)
err := postCollection.Find(bson.M{"$text": bson.M{"$search": textSearch}}).Distinct("logId", &searchResults)
if err != nil {
return nil, err
}
// Posts always use shortId to refer to the log
query["shortId"] = bson.M{"$in": searchResults}
}
// Find logs including any of the specified events and channels
if len(channels) > 0 {
query["channel"] = bson.M{"$in": channels}
}
if len(events) > 0 {
query["events"] = bson.M{"$in": channels}
}
// Find logs including all of the specified character IDs.
if len(characterIds) > 0 {
query["characterIds"] = bson.M{"$all": characterIds}
}
// Limit to only open logs
if open {
query["open"] = true
}
return listLog(query, limit)
}
// Edit sets the metadata
func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
changes := bson.M{}
if title != nil && *title != log.Title {
changes["title"] = *title
}
if event != nil && *event != log.Event {
changes["event"] = *event
}
if description != nil && *description != log.Description {
changes["description"] = *description
}
if open != nil && *open != log.Open {
changes["open"] = *open
}
if len(changes) == 0 {
return nil
}
err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
if err != nil {
return err
}
if title != nil {
log.Title = *title
}
if event != nil {
log.Event = *event
}
if description != nil {
log.Description = *description
}
if open != nil {
log.Open = *open
}
return nil
}
// Posts gets all the posts under the log. If no kinds are specified, it
// will get all posts
func (log *Log) Posts(kinds ...string) ([]Post, error) {
postMutex.RLock()
defer postMutex.RUnlock()
query := bson.M{
"$or": []bson.M{
bson.M{"logId": log.ID},
bson.M{"logId": log.ShortID},
},
}
if len(kinds) > 0 {
for i := range kinds {
kinds[i] = strings.ToLower(kinds[i])
}
query["kind"] = bson.M{"$in": kinds}
}
posts, err := listPosts(query)
if err != nil {
return nil, err
}
sort.SliceStable(posts, func(i, j int) bool {
return posts[i].Position < posts[j].Position
})
return posts, nil
}
// NewPost creates a new post.
func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
if kind == "" || nick == "" || text == "" {
return Post{}, errors.New("Missing/empty parameters")
}
postMutex.RLock()
defer postMutex.RUnlock()
position, err := counter.Next("next_post_id", log.ShortID)
if err != nil {
return Post{}, err
}
post := Post{
ID: MakePostID(time),
Position: position,
LogID: log.ShortID,
Time: time,
Kind: kind,
Nick: nick,
Text: text,
}
err = postCollection.Insert(post)
if err != nil {
return Post{}, err
}
return post, nil
}
// UpdateCharacters updates the character list
func (log *Log) UpdateCharacters() error {
characterUpdateMutex.Lock()
defer characterUpdateMutex.Unlock()
posts, err := log.Posts()
if err != nil {
return err
}
added := make(map[string]bool)
removed := make(map[string]bool)
for _, post := range posts {
if post.Kind == "text" || post.Kind == "action" {
if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
continue
}
// Clean up the nick (remove possessive suffix, comma, formatting stuff)
if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
post.Nick = post.Nick[:len(post.Nick)-2]
} else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
post.Nick = post.Nick[:len(post.Nick)-1]
}
added[post.Nick] = true
}
if post.Kind == "chars" {
tokens := strings.Fields(post.Text)
for _, token := range tokens {
if strings.HasPrefix(token, "-") {
removed[token[1:]] = true
} else {
added[strings.Replace(token, "+", "", 1)] = true
}
}
}
}
nicks := make([]string, 0, len(added))
for nick := range added {
if added[nick] && !removed[nick] {
nicks = append(nicks, nick)
}
}
characters, err := character.ListNicks(nicks...)
if err != nil {
return err
}
characterIDs := make([]string, len(characters))
for i, char := range characters {
characterIDs[i] = char.ID
}
err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
if err != nil {
return err
}
for _, nick := range nicks {
found := false
for _, character := range characters {
if character.HasNick(nick) {
found = true
break
}
}
if !found {
addUnknownNick(nick)
}
}
log.CharacterIDs = characterIDs
return nil
}
// Remove removes the log and all associated posts from the database
func (log *Log) Remove() error {
err := logsCollection.Remove(bson.M{"_id": log.ID})
if err != nil {
return err
}
_, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{
bson.M{"logId": log.ID},
bson.M{"logId": log.ShortID},
}})
if err != nil {
return err
}
return nil
}
func findLog(query interface{}) (Log, error) {
log := Log{}
err := logsCollection.Find(query).One(&log)
if err != nil {
return Log{}, err
}
return log, nil
}
func listLog(query interface{}, limit int) ([]Log, error) {
logs := make([]Log, 0, 64)
err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
if err != nil {
return nil, err
}
return logs, nil
}
func iterLogs(query interface{}, limit int) *mgo.Iter {
return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
}
// MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
func MakeLogID(date time.Time, channel string) string {
return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
}
func init() {
store.HandleInit(func(db *mgo.Database) {
logsCollection = db.C("logbot3.logs")
logsCollection.EnsureIndexKey("date")
logsCollection.EnsureIndexKey("channel")
logsCollection.EnsureIndexKey("characterIds")
logsCollection.EnsureIndexKey("event")
logsCollection.EnsureIndex(mgo.Index{
Key: []string{"channel", "open"},
})
err := logsCollection.EnsureIndex(mgo.Index{
Key: []string{"shortId"},
Unique: true,
DropDups: true,
})
if err != nil {
log.Fatalln("init logbot3.logs:", err)
}
})
}
-198
View File
@@ -1,198 +0,0 @@
package log
import (
"crypto/rand"
"encoding/binary"
"errors"
"log"
"strconv"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var postCollection *mgo.Collection
// A Post is a part of a log file.
type Post struct {
ID string `bson:"_id"`
LogID string `bson:"logId"`
Time time.Time `bson:"time"`
Kind string `bson:"kind"`
Nick string `bson:"nick"`
Text string `bson:"text"`
Position int `bson:"position"`
}
// Edit the post
func (post *Post) Edit(time *time.Time, kind *string, nick *string, text *string) error {
changes := bson.M{}
changed := false
postCopy := *post
if time != nil && !time.IsZero() && !time.Equal(post.Time) {
changes["time"] = *time
changed = true
postCopy.Time = *time
}
if kind != nil && *kind != "" && *kind != post.Kind {
changes["kind"] = *kind
changed = true
postCopy.Kind = *kind
}
if nick != nil && *nick != "" && *nick != post.Nick {
changes["nick"] = *nick
changed = true
postCopy.Nick = *nick
}
if text != nil && *text != "" && *text != post.Text {
changes["text"] = *text
changed = true
postCopy.Text = *text
}
if !changed {
return nil
}
err := postCollection.UpdateId(post.ID, bson.M{"$set": changes})
if err != nil {
return err
}
*post = postCopy
return nil
}
// Move the post
func (post *Post) Move(toPosition int) error {
if toPosition < 1 {
return errors.New("Invalid position")
}
postMutex.Lock()
defer postMutex.Unlock()
// To avoid problems, only allow target indices that are allowed. If it's 1, then there is bound to
// be a post at the position.
if toPosition > 1 {
existingPost := Post{}
err := postCollection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existingPost)
if err != nil || existingPost.Position != toPosition {
return errors.New("No post found at the position")
}
}
query := bson.M{"logId": post.LogID}
operation := bson.M{"$inc": bson.M{"position": 1}}
if toPosition < post.Position {
query["$and"] = []bson.M{
bson.M{"position": bson.M{"$gte": toPosition}},
bson.M{"position": bson.M{"$lt": post.Position}},
}
} else {
query["$and"] = []bson.M{
bson.M{"position": bson.M{"$gt": post.Position}},
bson.M{"position": bson.M{"$lte": toPosition}},
}
operation["$inc"] = bson.M{"position": -1}
}
_, err := postCollection.UpdateAll(query, operation)
if err != nil {
return errors.New("moving others: " + err.Error())
}
err = postCollection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}})
if err != nil {
return errors.New("moving: " + err.Error())
}
post.Position = toPosition
return nil
}
// FindPostID finds a log post by ID.
func FindPostID(id string) (Post, error) {
return findPost(bson.M{"_id": id})
}
// ListPostIDs lists log posts by ID
func ListPostIDs(ids ...string) ([]Post, error) {
return listPosts(bson.M{"_id": bson.M{"$in": ids}})
}
// RemovePost removes a post, moving all subsequent post up one position
func RemovePost(id string) (Post, error) {
postMutex.Lock()
defer postMutex.Unlock()
post, err := findPost(bson.M{"_id": id})
if err != nil {
return Post{}, err
}
err = postCollection.RemoveId(id)
if err != nil {
return Post{}, err
}
_, err = postCollection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
if err != nil {
return Post{}, err
}
return post, nil
}
func findPost(query interface{}) (Post, error) {
post := Post{}
err := postCollection.Find(query).One(&post)
if err != nil {
return Post{}, err
}
return post, nil
}
func listPosts(query interface{}) ([]Post, error) {
posts := make([]Post, 0, 64)
err := postCollection.Find(query).All(&posts)
if err != nil {
return nil, err
}
return posts, nil
}
// MakePostID makes a random post ID
func MakePostID(time time.Time) string {
data := make([]byte, 4)
rand.Read(data)
return "P" + strconv.FormatInt(time.UnixNano(), 36) + strconv.FormatInt(int64(binary.LittleEndian.Uint32(data)), 36)
}
func init() {
store.HandleInit(func(db *mgo.Database) {
postCollection = db.C("logbot3.posts")
postCollection.EnsureIndexKey("logId")
postCollection.EnsureIndexKey("time")
postCollection.EnsureIndexKey("kind")
postCollection.EnsureIndexKey("position")
err := postCollection.EnsureIndex(mgo.Index{
Key: []string{"$text:text"},
})
if err != nil {
log.Fatalln("init logbot3.logs:", err)
}
})
}
-42
View File
@@ -1,42 +0,0 @@
package log
import (
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var unknownConnection *mgo.Collection
// An UnknownNick is a nick found by the character list updater that
// does not exist. The score is the number of logs that nick was in, meaning
// nicks with a higher score should be a high priority to be matched with
// a character.
type UnknownNick struct {
Nick string `bson:"_id" json:"nick"`
Score int `bson:"score" json:"score"`
}
// UnknownNicks gets all the unknown nicks from the last search.
func UnknownNicks() ([]UnknownNick, error) {
nicks := make([]UnknownNick, 0, 256)
err := unknownConnection.Find(bson.M{}).Sort("-score").All(&nicks)
return nicks, err
}
func addUnknownNick(nick string) error {
_, err := unknownConnection.UpsertId(nick, bson.M{"$inc": bson.M{"score": 1}})
return err
}
func clearUnknownNicks() error {
_, err := unknownConnection.RemoveAll(bson.M{})
return err
}
func init() {
store.HandleInit(func(db *mgo.Database) {
unknownConnection = db.C("logbot3.unknown_nicks")
})
}
-84
View File
@@ -1,84 +0,0 @@
package log
import (
"sync"
"time"
"github.com/globalsign/mgo/bson"
)
var scheduleCharacterUpdate = func() func() {
var mutex sync.Mutex
var scheduled bool
return func() {
mutex.Lock()
if !scheduled {
go func() {
time.Sleep(time.Second * 60)
// If another comes along in the next 2-3 seconds, it should schedule a new
// round to avoid a character only appearing in half their logs.
mutex.Lock()
scheduled = false
mutex.Unlock()
UpdateAllCharacters()
}()
scheduled = true
}
mutex.Unlock()
}
}()
// ScheduleCharacterUpdate schedules a full update within the minute.
// Subsequent calls within that time will not schedule anything. Even
// if the operation takes a few seconds at most, it need not be ran often.
func ScheduleCharacterUpdate() {
scheduleCharacterUpdate()
}
// UpdateCharacters is a shorthand for getting a log and updaing its characters
func UpdateCharacters(logID string) error {
log, err := FindID(logID)
if err != nil {
return err
}
return log.UpdateCharacters()
}
// UpdateAllCharacters updates character list on all logs. This should
// be done if one or more characters failed to be added.
func UpdateAllCharacters() (updated int, err error) {
updated = 0
err = clearUnknownNicks()
if err != nil {
return
}
iter := iterLogs(bson.M{}, 0)
err = iter.Err()
if err != nil {
return
}
log := Log{}
for iter.Next(&log) {
err = log.UpdateCharacters()
if err != nil {
return
}
updated++
}
err = iter.Err()
if err != nil {
return
}
return
}
-111
View File
@@ -1,111 +0,0 @@
package story
import (
"crypto/rand"
"encoding/binary"
"strconv"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var chapterCollection *mgo.Collection
// A Chapter is a part of a story.
type Chapter struct {
ID string `bson:"_id"`
StoryID string `bson:"storyId"`
Title string `bson:"title"`
Author string `bson:"author"`
Source string `bson:"source"`
CreatedDate time.Time `bson:"createdDate"`
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
EditedDate time.Time `bson:"editedDate"`
}
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
// change, this functill will due to EditedDate.
func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) error {
now := time.Now()
changes := bson.M{"editedDate": now}
changed := *chapter
changed.EditedDate = now
if title != nil && *title != chapter.Title {
changes["title"] = *title
changed.Title = *title
}
if source != nil && *source != chapter.Source {
changes["source"] = *source
changed.Source = *source
}
if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
err := chapterCollection.UpdateId(chapter.ID, bson.M{"$set": changes})
if err != nil {
return err
}
*chapter = changed
return nil
}
// Remove removes a chapter.
func (chapter *Chapter) Remove() error {
return chapterCollection.RemoveId(chapter.ID)
}
// FindChapterID finds a chapter by its own ID
func FindChapterID(id string) (Chapter, error) {
chapter := Chapter{}
err := chapterCollection.FindId(id).One(&chapter)
return chapter, err
}
// ListChapterStoryID lists all chapters for the story ID
func ListChapterStoryID(storyID string) ([]Chapter, error) {
chapters := make([]Chapter, 0, 8)
err := chapterCollection.Find(bson.M{"storyId": storyID}).Sort("createdDate").All(&chapters)
if err != nil {
return nil, err
}
return chapters, nil
}
// makeChapterID makes a random chapter ID that's 24 characters long
func makeChapterID() string {
result := "SC"
offset := 0
data := make([]byte, 32)
rand.Read(data)
for len(result) < 24 {
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
offset += 8
if offset >= 32 {
rand.Read(data)
offset = 0
}
}
return result[:24]
}
func init() {
store.HandleInit(func(db *mgo.Database) {
chapterCollection = db.C("story.chapters")
chapterCollection.EnsureIndexKey("storyId")
chapterCollection.EnsureIndexKey("author")
chapterCollection.EnsureIndexKey("createdDate")
})
}
-258
View File
@@ -1,258 +0,0 @@
package story
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"os"
"strconv"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var storyCollection *mgo.Collection
// ErrTagAlreadyExists is an error returned by Story.AddTag
var ErrTagAlreadyExists = errors.New("Tag already exists")
// ErrTagNotExists is an error returned by Story.RemoveTag
var ErrTagNotExists = errors.New("Tag does not exist")
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
// The story model is a container for multiple chapters this time, in contrast to the previous version.
type Story struct {
ID string `bson:"_id"`
Author string `bson:"author"`
Name string `bson:"name"`
Category string `bson:"category"`
Open bool `bson:"open"`
Listed bool `bson:"listed"`
Tags []Tag `bson:"tags"`
CreatedDate time.Time `bson:"createdDate"`
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
UpdatedDate time.Time `bson:"updatedDate"`
}
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
func (story *Story) AddTag(tag Tag) error {
for i := range story.Tags {
if story.Tags[i].Equal(tag) {
return ErrTagAlreadyExists
}
}
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}})
if err != nil {
return err
}
story.Tags = append(story.Tags, tag)
return nil
}
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
func (story *Story) RemoveTag(tag Tag) error {
index := -1
for i := range story.Tags {
if story.Tags[i].Equal(tag) {
index = i
break
}
}
if index == -1 {
return ErrTagNotExists
}
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}})
if err != nil {
return err
}
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
return nil
}
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
// changed, it will silently return without a database roundtrip.
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error {
changes := bson.M{}
changed := *story
if name != nil && *name != story.Name {
changes["name"] = *name
changed.Name = *name
}
if category != nil && *category != story.Category {
changes["category"] = *category
changed.Name = *category
}
if listed != nil && *listed != story.Listed {
changes["listed"] = *listed
changed.Listed = *listed
}
if open != nil && *open != story.Open {
changes["open"] = *open
changed.Open = *open
}
if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
if len(changes) == 0 {
return nil
}
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes})
if err != nil {
return err
}
*story = changed
return nil
}
// Remove the story from the database
func (story *Story) Remove() error {
return storyCollection.RemoveId(story.ID)
}
// Chapters calls ListChapterStoryID with the story's ID:
func (story *Story) Chapters() ([]Chapter, error) {
return ListChapterStoryID(story.ID)
}
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
// occurs
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
if !story.Open && author != story.Author {
fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title)
}
chapter := Chapter{
ID: makeChapterID(),
StoryID: story.ID,
Title: title,
Author: author,
Source: source,
CreatedDate: createdDate,
FictionalDate: finctionalDate,
EditedDate: createdDate,
}
err := chapterCollection.Insert(chapter)
if err != nil {
return Chapter{}, err
}
if createdDate.After(story.UpdatedDate) {
if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
story.UpdatedDate = createdDate
}
}
return chapter, nil
}
// New creates a new story.
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
story := Story{
ID: makeStoryID(),
Name: name,
Author: author,
Category: category,
Listed: listed,
Open: open,
Tags: tags,
CreatedDate: createdDate,
FictionalDate: fictionalDate,
UpdatedDate: createdDate,
}
err := storyCollection.Insert(story)
if err != nil {
return Story{}, err
}
return story, nil
}
// FindID finds a story by ID
func FindID(id string) (Story, error) {
story := Story{}
err := storyCollection.FindId(id).One(&story)
return story, err
}
// List lists stories by any non-zero criteria passed with it.
func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) {
query := bson.M{}
if author != "" {
query["author"] = author
}
if len(tags) > 0 {
query["tags"] = bson.M{"$in": tags}
}
if !earliest.IsZero() && !latest.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": earliest,
"$lt": latest,
}
} else if !latest.IsZero() {
query["fictionalDate"] = bson.M{
"$lt": latest,
}
} else if !earliest.IsZero() {
query["fictionalDate"] = bson.M{
"$gte": earliest,
}
}
stories := make([]Story, 0, 128)
err := storyCollection.Find(query).Limit(limit).All(&stories)
return stories, err
}
// makeStoryID makes a random story ID that's 16 characters long
func makeStoryID() string {
result := "S"
offset := 0
data := make([]byte, 32)
rand.Read(data)
for len(result) < 16 {
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
offset += 8
if offset >= 32 {
rand.Read(data)
offset = 0
}
}
return result[:16]
}
func init() {
store.HandleInit(func(db *mgo.Database) {
storyCollection = db.C("story.stories")
storyCollection.EnsureIndexKey("tags")
storyCollection.EnsureIndexKey("author")
storyCollection.EnsureIndexKey("updatedDate")
storyCollection.EnsureIndexKey("fictionalDate")
storyCollection.EnsureIndexKey("listed")
})
}
-36
View File
@@ -1,36 +0,0 @@
package story
import (
"sort"
"strings"
"github.com/globalsign/mgo/bson"
)
// A Tag associates a story with other content, like other stories, logs and more.
type Tag struct {
Kind string `bson:"kind"`
Name string `bson:"name"`
}
// Equal returns true if the tags match one another.
func (tag *Tag) Equal(other Tag) bool {
return tag.Kind == other.Kind && tag.Name == other.Name
}
// ListTags lists all tags
func ListTags() ([]Tag, error) {
tags := make([]Tag, 0, 64)
err := storyCollection.Find(bson.M{"listed": true, "tags": bson.M{"$ne": nil}}).Distinct("tags", &tags)
sort.Slice(tags, func(i, j int) bool {
kindCmp := strings.Compare(tags[i].Kind, tags[j].Kind)
if kindCmp != 0 {
return kindCmp < 0
}
return strings.Compare(tags[i].Name, tags[j].Name) < 0
})
return tags, err
}
+63
View File
@@ -0,0 +1,63 @@
package models
import (
"fmt"
"io"
"strconv"
)
// ChangeModel describes a model related to the change.
type ChangeModel string
const (
// ChangeModelCharacter is a value of ChangeModel
ChangeModelCharacter ChangeModel = "Character"
// ChangeModelChannel is a value of ChangeModel
ChangeModelChannel ChangeModel = "Channel"
// ChangeModelLog is a value of ChangeModel
ChangeModelLog ChangeModel = "Log"
// ChangeModelPost is a value of ChangeModel
ChangeModelPost ChangeModel = "Post"
// ChangeModelStory is a value of ChangeModel
ChangeModelStory ChangeModel = "Story"
// ChangeModelTag is a value of ChangeModel
ChangeModelTag ChangeModel = "Tag"
// ChangeModelChapter is a value of ChangeModel
ChangeModelChapter ChangeModel = "Chapter"
// ChangeModelComment is a value of ChangeModel
ChangeModelComment ChangeModel = "Comment"
// ChangeModelFile is a value of ChangeModel
ChangeModelFile ChangeModel = "File"
)
// IsValid returns true if the underlying string is one of the correct values.
func (e ChangeModel) IsValid() bool {
switch e {
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter, ChangeModelComment:
return true
}
return false
}
func (e ChangeModel) String() string {
return string(e)
}
// UnmarshalGQL unmarshals the underlying graphql value.
func (e *ChangeModel) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ChangeModel(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ChangeModel", str)
}
return nil
}
// MarshalGQL marshals the underlying graphql value.
func (e ChangeModel) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
+183
View File
@@ -0,0 +1,183 @@
package models
import (
"reflect"
"time"
)
// Change represents a change in the rpdata history through the API.
type Change struct {
ID string `bson:"_id"`
Model ChangeModel `bson:"model"`
Op string `bson:"op"`
Author string `bson:"author"`
Listed bool `bson:"listed"`
Keys []ChangeKey `bson:"keys"`
Date time.Time `bson:"date"`
Logs []*Log `bson:"logs"`
Characters []*Character `bson:"characters"`
Channels []*Channel `bson:"channels"`
Posts []*Post `bson:"posts"`
Stories []*Story `bson:"stories"`
Tags []*Tag `bson:"tags"`
Chapters []*Chapter `bson:"chapters"`
Comments []*Comment `bson:"comments"`
Files []*File `bson:"files"`
}
// AddObject adds the model into the appropriate array.
func (change *Change) AddObject(object interface{}) bool {
if v := reflect.ValueOf(object); v.Kind() == reflect.Struct {
ptr := reflect.PtrTo(v.Type())
ptrValue := reflect.New(ptr.Elem())
ptrValue.Elem().Set(v)
object = ptrValue.Interface()
}
switch object := object.(type) {
case *Log:
change.Logs = append(change.Logs, object)
case []*Log:
change.Logs = append(change.Logs, object...)
case *Character:
change.Characters = append(change.Characters, object)
case []*Character:
change.Characters = append(change.Characters, object...)
case *Channel:
change.Channels = append(change.Channels, object)
case []*Channel:
change.Channels = append(change.Channels, object...)
case *Post:
change.Posts = append(change.Posts, object)
case []*Post:
change.Posts = append(change.Posts, object...)
case *Story:
change.Stories = append(change.Stories, object)
case []*Story:
change.Stories = append(change.Stories, object...)
case *Tag:
change.Tags = append(change.Tags, object)
case []*Tag:
change.Tags = append(change.Tags, object...)
case *Chapter:
change.Chapters = append(change.Chapters, object)
case []*Chapter:
change.Chapters = append(change.Chapters, object...)
case *Comment:
change.Comments = append(change.Comments, object)
case []*Comment:
change.Comments = append(change.Comments, object...)
case *File:
change.Files = append(change.Files, object)
case []*File:
change.Files = append(change.Files, object...)
default:
return false
}
return true
}
// Objects makes a combined, mixed array of all the models stored in this change.
func (change *Change) Objects() []interface{} {
data := make([]interface{}, 0, 8)
for _, log := range change.Logs {
data = append(data, log)
}
for _, channel := range change.Channels {
data = append(data, channel)
}
for _, character := range change.Characters {
data = append(data, character)
}
for _, post := range change.Posts {
data = append(data, post)
}
for _, story := range change.Stories {
data = append(data, story)
}
for _, tag := range change.Tags {
data = append(data, tag)
}
for _, chapter := range change.Chapters {
data = append(data, chapter)
}
for _, comment := range change.Comments {
data = append(data, comment)
}
for _, file := range change.Files {
data = append(data, file)
}
return data
}
func (change *Change) PassesFilter(filter ChangeFilter) bool {
if filter.Author != nil && change.Author != *filter.Author {
return false
}
// For unlisted changes, pass it only if the filter refers to the specific index.
if !change.Listed {
hasSpecificKey := false
KeyFindLoop:
for _, key := range filter.Keys {
if key.ID == "*" {
continue
}
for _, changeKey := range change.Keys {
if changeKey.Model == key.Model && changeKey.ID == key.ID {
hasSpecificKey = true
break KeyFindLoop
}
}
}
if !hasSpecificKey {
return false
}
}
if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) {
return false
}
if len(filter.Keys) > 0 {
foundKey := false
KeyFindLoop2:
for _, key := range filter.Keys {
for _, changeKey := range change.Keys {
if changeKey == key {
foundKey = true
break KeyFindLoop2
}
}
}
if !foundKey {
return false
}
}
return true
}
// ChangeKey is a key for a change that can be used when subscribing to them.
type ChangeKey struct {
Model ChangeModel `bson:"model"`
ID string `bson:"id"`
}
// ChangeFilter is a filter for listing changes.
type ChangeFilter struct {
Keys []ChangeKey
EarliestDate *time.Time
Author *string
Limit *int
}

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