const { UserInputError, AuthenticationError, gql, } = require("apollo-server-express") const shuffle = require("../utils/shuffleArray") const User = require("../mongoose-models/user") const Suggested = require("../mongoose-models/suggested") const Summary = require("../mongoose-models/summary") const VotedPerson = require("../mongoose-models/votedperson") const State = require("../mongoose-models/state") const Player = require("../mongoose-models/player") const Placement = require("../mongoose-models/placement") const typeDef = gql` extend type Query { plusInfo: PlusGeneralInfo hasAccess(discord_id: String!): String xPowers(discord_id: String!): [Int]! suggestions: [Suggested!] vouches: [User!] usersForVoting: UsersForVoting! summaries: [Summary!] } extend type Mutation { addSuggestion( discord_id: String! server: String! region: String! description: String! ): Boolean! addVouch(discord_id: String!, server: String!, region: String!): Boolean! addVotes(votes: [VoteInput!]!): Boolean! startVoting(ends: String!): Boolean! endVoting: Boolean! } "+1 or +2 LFG server on Discord" enum PlusServer { ONE TWO } "Region used for voting" enum PlusRegion { EU NA } type PlusGeneralInfo { voting_ends: String voter_count: Int! eligible_voters: Int! } type Suggested { discord_id: String! discord_user: User! suggester_discord_id: String! suggester_discord_user: User! plus_region: PlusRegion! plus_server: PlusServer! description: String! createdAt: String! } "Status with +1 and +2 related things" type PlusStatus { membership_status: PlusServer vouch_status: PlusServer plus_region: PlusRegion can_vouch: PlusServer voucher_discord_id: String voucher_user: User can_vouch_again_after: String } extend type User { plus: PlusStatus } input VoteInput { discord_id: String! score: Int! } type VotedPerson { discord_id: String! voter_discord_id: String! plus_server: String! month: Int! year: Int! "Voting result -2 to +2 (-1 to +1 cross-region)" score: Int! stale: Boolean! } type Score { total: Float! eu_count: [Int] na_count: [Int] } "Voting result of a player" type Summary { discord_id: String! discord_user: User! plus_server: PlusServer! month: Int! year: Int! suggested: Boolean vouched: Boolean "Average of all scores of the voters for the month 0% to 100%" score: Score! new: Boolean! } type UsersForVoting { users: [User!]! suggested: [Suggested!]! votes: [VotedPerson!]! } ` const validateVotes = (votes, users, suggested, user) => { const region = user.plus.plus_region votes.forEach((vote) => { const { discord_id, score } = vote let votedUser = users.find( (userInServer) => userInServer.discord_id === discord_id ) if (!votedUser) { votedUser = suggested.find( (suggested) => suggested.discord_user.discord_id === discord_id ) if (!votedUser) throw new UserInputError( `Invalid user voted on with the id ${discord_id}` ) const plus_region_of_suggested = votedUser.plus_region votedUser = votedUser.discord_user votedUser.plus = {} votedUser.plus.plus_region = plus_region_of_suggested } if (score !== -2 && score !== -1 && score !== 1 && score !== 2) throw new Error(`Invalid score provided: ${score}`) if ((score === -2 || score === 2) && region !== votedUser.plus.plus_region) throw new Error("Score of -2 or 2 given cross region") }) } const resolvers = { Query: { hasAccess: async (root, args) => { const user = await User.findOne({ discord_id: args.discord_id }).catch( (e) => { throw ( (new Error(), { invalidArgs: args, }) ) } ) if (!user || !user.plus) return false const { membership_status, vouch_status } = user.plus let membership_code = membership_status === "TWO" || vouch_status === "TWO" ? "TWO" : null membership_code = membership_status === "ONE" || vouch_status === "ONE" ? "ONE" : membership_code return membership_code }, plusInfo: async (root, args, ctx) => { if (!ctx.user) return null if (!ctx.user.plus || !ctx.user.plus.membership_status) { return null } const state = await State.findOne({}) const votedPeople = await VotedPerson.find({ stale: false, plus_server: ctx.user.plus.membership_status, }) const votedIds = new Set() votedPeople.forEach((vote) => { votedIds.add(vote.voter_discord_id) }) const eligible_voters = await User.countDocuments({ "plus.membership_status": ctx.user.plus.membership_status, }) return { voting_ends: state.voting_ends, voter_count: votedIds.size, eligible_voters, } }, xPowers: async (root, args, ctx) => { const user = await User.findOne({ discord_id: args.discord_id }) if (!user || !user.twitter_name) return [null, null, null, null] const twitter = user.twitter_name.toLowerCase() const player = await Player.findOne({ twitter, }) if (!player) return [null, null, null, null] const placements = await Placement.find({ unique_id: player.unique_id }) return placements.reduce( (acc, cur) => { const modeIndex = cur.mode - 1 const xPower = Math.floor(cur.x_power / 100) * 100 if (acc[modeIndex] === null) { acc[modeIndex] = xPower } else if (xPower > acc[modeIndex]) { acc[modeIndex] = xPower } return acc }, [null, null, null, null] ) }, usersForVoting: async (root, args, ctx) => { if (!ctx.user) throw new UserInputError("Not logged in") if (!ctx.user.plus || !ctx.user.plus.membership_status) throw new UserInputError("Not plus server member") const plus_server = ctx.user.plus.membership_status const users = await User.find({ $or: [ { "plus.membership_status": plus_server, }, { "plus.vouch_status": plus_server }, ], }).catch((e) => { throw ( (new Error(), { error: e, }) ) }) const suggested = await Suggested.find({ plus_server }) .populate("discord_user") .populate("suggester_discord_user") .catch((e) => { throw ( (new Error(), { error: e, }) ) }) const votes = await VotedPerson.find({ voter_discord_id: ctx.user.discord_id, }) shuffle(users) shuffle(suggested) return { users, suggested, votes } }, suggestions: (root, args, ctx) => { if (!ctx.user || !ctx.user.plus || !ctx.user.plus.membership_status) return null const searchCriteria = ctx.user.plus.membership_status === "ONE" ? {} : { plus_server: "TWO" } return Suggested.find(searchCriteria) .populate("discord_user") .populate("suggester_discord_user") .sort({ plus_server: "asc", createdAt: "desc" }) .catch((e) => { throw new UserInputError(e.message, { invalidArgs: args, }) }) }, vouches: (root, args, { user }) => { if (!user || !user.plus || !user.plus.membership_status) return null const searchCriteria = user.plus.membership_status === "ONE" ? { "plus.vouch_status": { $ne: null } } : { "plus.vouch_status": "TWO" } return User.find(searchCriteria) .sort({ "plus.vouch_status": "asc" }) .populate("plus.voucher_user") }, summaries: (root, args, ctx) => { if (!ctx.user || !ctx.user.plus || !ctx.user.plus.membership_status) return null const searchCriteria = ctx.user.plus.membership_status === "ONE" ? {} : { plus_server: "TWO" } return Summary.find(searchCriteria) .populate("discord_user") .sort({ year: "desc", month: "desc", "score.total": "desc" }) }, }, Mutation: { addSuggestion: async (root, args, ctx) => { if (!ctx.user) throw new AuthenticationError("Not logged in.") if (!ctx.user.plus || !ctx.user.plus.membership_status) { throw new AuthenticationError("Not plus member.") } const state = await State.findOne({}) if (state && !!state.voting_ends) { throw new UserInputError( "Voting already started so suggesting not possible" ) } const user = await User.findOne({ discord_id: args.discord_id }).catch( (e) => { throw ( (new Error(), { invalidArgs: args, error: e, }) ) } ) const suggestion = await Suggested.findOne({ suggester_discord_id: ctx.user.discord_id, }).catch((e) => { throw ( (new Error(), { invalidArgs: args, error: e, }) ) }) if (suggestion) throw new UserInputError("Already suggested this month.") const duplicateSuggestion = await Suggested.findOne({ discord_id: args.discord_id, plus_server: args.server, }).catch((e) => { throw ( (new Error(), { invalidArgs: args, error: e, }) ) }) if (duplicateSuggestion) throw new UserInputError( "This user has already been suggested this month." ) if (!user) throw new UserInputError("Suggested user not sendou.ink member.") if (args.server !== "ONE" && args.server !== "TWO") throw new UserInputError("Server arg has to be 'ONE' or 'TWO'.") if ( user.plus.membership_status === args.server || user.plus.membership_status === "ONE" || user.plus.vouch_status === args.server || user.plus.vouch_status === "ONE" ) throw new UserInputError( "Suggested user is already a member of the server." ) const date = new Date() const month = date.getMonth() + 1 const year = date.getFullYear() const kickedSummary = await Summary.findOne({ discord_id: args.discord_id, plus_server: args.server, suggested: { $in: [null, false] }, month, year, score: { $lt: 0 }, }) if (kickedSummary) { throw new UserInputError( "Can't suggest because user got kicked less than month ago." ) } if (ctx.user.plus.membership_status !== "ONE" && args.server === "ONE") throw new UserInputError("Can't suggest to +1 without being +1 member.") if (args.region !== "EU" && args.region !== "NA") throw new UserInputError("Region arg has to be 'NA' or 'EU'.") if (args.description.length > 1000) throw new UserInputError("Description has to be below 1000 characters.") const newSuggestion = new Suggested({ discord_id: args.discord_id, suggester_discord_id: ctx.user.discord_id, plus_region: args.region, plus_server: args.server, description: args.description, }) await newSuggestion.save().catch((e) => { throw ( (new Error(), { invalidArgs: args, }) ) }) return true }, addVouch: async (root, args, ctx) => { if (!ctx.user) throw new AuthenticationError("Not logged in.") if (!ctx.user.plus || !ctx.user.plus.membership_status) { throw new AuthenticationError("Not plus member.") } const state = await State.findOne({}) if (state && !!state.voting_ends) { throw new UserInputError( "Voting already started so suggesting not possible" ) } if (args.server !== "ONE" && args.server !== "TWO") throw new UserInputError("Invalid plus server given.") if (args.region !== "EU" && args.region !== "NA") throw new UserInputError("Invalid region given.") const can_vouch = ctx.user.plus.can_vouch if (!can_vouch || (can_vouch !== "ONE" && args.server === "ONE")) throw new UserInputError("No privileges to vouch.") if (ctx.user.plus.can_vouch_again_after) throw new UserInputError( "No privileges to vouch right now due to previous vouch getting kicked." ) const user = await User.findOne({ discord_id: args.discord_id }) if (!user) throw new UserInputError("User vouched is not a sendou.ink member.") if ( user.plus && (user.plus.membership_status === args.server || user.plus.vouch_status === args.server || user.plus.membership_status === "ONE" || user.plus.vouch_status === "ONE") ) throw new UserInputError("User already has access.") const date = new Date() const month = date.getMonth() + 1 const year = date.getFullYear() const kickedSummary = await Summary.findOne({ discord_id: args.discord_id, plus_server: args.server, suggested: { $in: [null, false] }, month, year, score: { $lt: 0 }, }) if (kickedSummary) { throw new UserInputError( "Can't vouch because user got kicked less than month ago." ) } if (!user.plus) user.plus = {} user.plus.vouch_status = args.server user.plus.voucher_discord_id = ctx.user.discord_id if (!user.plus.plus_region) user.plus.plus_region = args.region const vouchingUser = await User.findOne({ discord_id: ctx.user.discord_id, }) vouchingUser.plus.can_vouch = undefined await user.save() await vouchingUser.save() await Suggested.deleteOne({ discord_id: args.discord_id, plus_server: args.server, }) return true }, addVotes: async (root, args, ctx) => { if (!ctx.user) throw new AuthenticationError("Not logged in.") if (!ctx.user.plus || !ctx.user.plus.membership_status) { throw new AuthenticationError("Not plus member.") } const state = await State.findOne({}) const date = new Date() if (!state.voting_ends || state.voting_ends < date.getTime()) throw new UserInputError("Voting is not open right now") const votedUsers = {} args.votes.forEach((vote) => { if (votedUsers[vote.discord_id]) throw new UserInputVote( `Duplicate vote with the id ${vote.discord_id}` ) votedUsers[vote.discord_id] = true }) const plus_server = ctx.user.plus.membership_status const users = await User.find({ $or: [ { "plus.membership_status": plus_server, }, { "plus.vouch_status": plus_server }, ], }) const suggested = await Suggested.find({ plus_server }) .populate("discord_user") .populate("suggester_discord_user") if (users.length + suggested.length !== args.votes.length) throw new UserInputError("Invalid number of votes provided") validateVotes(args.votes, users, suggested, ctx.user) const year = date.getFullYear() const month = date.getMonth() + 1 await VotedPerson.deleteMany({ voter_discord_id: ctx.user.discord_id, stale: false, }) const toInsert = args.votes.map((vote) => ({ discord_id: vote.discord_id, voter_discord_id: ctx.user.discord_id, month, year, plus_server, score: vote.score, stale: false, })) await VotedPerson.insertMany(toInsert) return true }, startVoting: async (root, args, ctx) => { if (!ctx.user) throw new AuthenticationError("Not logged in.") if (ctx.user.discord_id !== process.env.ADMIN_ID) throw new AuthenticationError("Not admin.") await State.findOneAndUpdate({}, { voting_ends: args.ends }) return true }, endVoting: async (root, args, ctx) => { if (!ctx.user) throw new AuthenticationError("Not logged in.") if (ctx.user.discord_id !== process.env.ADMIN_ID) throw new AuthenticationError("Not admin.") const date = new Date() const year = date.getFullYear() const month = date.getMonth() + 1 const votes = await VotedPerson.find({ stale: false }) .populate("discord_user") .populate("voter_discord_user") const suggested = await Suggested.find({}).populate("discord_user") const plus_one_voted = {} const plus_two_voted = {} votes.forEach((vote) => { const { discord_id, plus_server, score, discord_user, voter_discord_user, } = vote let voted_plus_region = discord_user.plus.plus_region const voter_plus_region = voter_discord_user.plus.plus_region if (!voted_plus_region) { const suggestedUser = suggested.find( (suggested) => suggested.discord_id === discord_id ) voted_plus_region = suggestedUser.plus_region } const plus_x_voted = plus_server === "ONE" ? plus_one_voted : plus_two_voted if (!plus_x_voted.hasOwnProperty(discord_id)) { plus_x_voted[discord_id] = {} plus_x_voted[discord_id].same_region = [] plus_x_voted[discord_id].other_region = [] } if (!plus_x_voted[discord_id].plus_region) { let plus_region = discord_user.plus && discord_user.plus.plus_region ? discord_user.plus.plus_region : null if (!plus_region) plus_region = suggested.find( (suggester) => suggester.discord_id === discord_id ).plus_region plus_x_voted[discord_id].plus_region = plus_region } if (!plus_x_voted[discord_id].membership_status) { const membership_status = discord_user.plus && discord_user.plus.membership_status ? discord_user.plus.membership_status : null plus_x_voted[discord_id].membership_status = membership_status } if ( !plus_x_voted[discord_id].can_not_vouch && discord_user.plus && discord_user.plus.can_vouch_again_after ) { if ( parseInt(discord_user.plus.can_vouch_again_after) > new Date().getTime() ) plus_x_voted[discord_id].can_not_vouch = true } if ( !plus_x_voted[discord_id].voucher_discord_id && discord_user.plus && discord_user.plus.voucher_discord_id && plus_server === discord_user.plus.vouch_status ) { plus_x_voted[discord_id].voucher_discord_id = discord_user.plus.voucher_discord_id } const arrToPushTo = voted_plus_region === voter_plus_region ? plus_x_voted[discord_id].same_region : plus_x_voted[discord_id].other_region arrToPushTo.push(score) }) const votingArrays = [plus_one_voted, plus_two_voted] const summariesToInsert = [] const userUpdates = [] const preventVouchingUpdates = [] votingArrays.forEach((votingArray, index) => { const arrays_plus_server = index === 0 ? "ONE" : "TWO" Object.keys(votingArray).forEach((discord_id) => { const { same_region, other_region, plus_region, membership_status, can_not_vouch, voucher_discord_id, dont_update, } = votingArray[discord_id] const same_total = same_region.reduce((acc, cur) => acc + cur) const other_total = other_region.reduce((acc, cur) => acc + cur) const total_score = +( ((same_total / same_region.length + other_total / other_region.length + 3) / 6) * 100 ).toFixed(2) const countReducer = (acc, cur) => { const scoreMap = { "-2": 0, "-1": 1, "1": 2, "2": 3 } const scoreIndex = scoreMap[cur] if (!acc[scoreIndex]) acc[scoreIndex] = 1 else acc[scoreIndex] = acc[scoreIndex] + 1 return acc } const same_count = same_region.reduce(countReducer, [0, 0, 0, 0]) const other_count = other_region.reduce(countReducer, [0, 0, 0, 0]) const summary = { discord_id, plus_server: arrays_plus_server, month, year, score: { total: total_score, eu_count: plus_region === "EU" ? same_count : other_count, na_count: plus_region === "EU" ? other_count : same_count, }, } if (total_score < 50 && membership_status === arrays_plus_server) { if (membership_status === "TWO") userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.membership_status": null } } ) ) else userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.membership_status": "TWO" } } ) ) } else if ( total_score >= 50 && membership_status !== arrays_plus_server && !dont_update ) { userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.membership_status": arrays_plus_server } } ) ) if (arrays_plus_server === "ONE" && plus_two_voted[discord_id]) { plus_two_voted[discord_id].dont_update = true } } if (voucher_discord_id) { summary.vouched = true if (total_score < 50) { const now = new Date() const can_vouch_again_after = new Date( now.getFullYear(), now.getMonth() + 5, 1 ) preventVouchingUpdates.push(() => User.updateOne( { discord_id: voucher_discord_id }, { $set: { "plus.can_vouch_again_after": can_vouch_again_after, "plus.can_vouch": null, }, } ) ) } } if (!voucher_discord_id && membership_status !== arrays_plus_server) { summary.suggested = true userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.plus_region": plus_region } } ) ) } if ( arrays_plus_server === "ONE" && total_score >= 90 && !can_not_vouch ) { userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.can_vouch": "ONE" } } ) ) } else if ( arrays_plus_server === "TWO" && total_score >= 80 && !can_not_vouch ) { userUpdates.push(() => User.updateOne( { discord_id }, { $set: { "plus.can_vouch": "TWO" } } ) ) } summariesToInsert.push(summary) }) }) await User.updateMany( { "plus.can_vouch": { $exists: true } }, { $set: { "plus.can_vouch": null } } ) await User.updateMany( { "plus.can_vouch_again_after": { $lte: new Date() } }, { $set: { "plus.can_vouch_again_after": null } } ) await User.updateMany( { "plus.vouch_status": { $exists: true } }, { $set: { "plus.vouch_status": null } } ) await User.updateMany( { "plus.voucher_discord_id": { $exists: true } }, { $set: { "plus.voucher_discord_id": null } } ) userUpdates.forEach(async (userUpdateFunction) => { await userUpdateFunction() }) preventVouchingUpdates.forEach(async (userUpdateFunction) => { await userUpdateFunction() }) await Summary.insertMany(summariesToInsert) await Suggested.deleteMany({}) await VotedPerson.deleteMany({ stale: true }) await VotedPerson.updateMany({}, { $set: { stale: true } }) await State.updateOne({}, { $set: { voting_ends: null } }) return true }, }, } module.exports = { Plus: typeDef, plusResolvers: resolvers, }