Skip to content

Commit

Permalink
feat: The big #$!? update.
Browse files Browse the repository at this point in the history
Too many changes to list individually so here's the short version:
Services/Ws takes care of websocket related entries.
Services/Handler takes care of the most logic given to it by Services/Ws
  • Loading branch information
KararTY committed Aug 10, 2020
1 parent 4acdece commit 37ee515
Show file tree
Hide file tree
Showing 13 changed files with 855 additions and 313 deletions.
6 changes: 1 addition & 5 deletions app/Controllers/Http/ProfilesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,8 @@ export default class ProfilesController {
return response.notFound({ error: this.Error.notFound })
}

const serializedProfile = profile.serialize()

serializedProfile.mismatches.push(match.id)

// We don't want to attempt to match with this user again in the future, so add them to the mismatches list.
profile.mismatches = serializedProfile.mismatches
profile.mismatches.push(match.id)

await profile.save()

Expand Down
289 changes: 289 additions & 0 deletions app/Services/Handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import Logger from '@ioc:Adonis/Core/Logger'
import Database from '@ioc:Adonis/Lucid/Database'
import Profile from 'App/Models/Profile'
import User from 'App/Models/User'
import { BASE, MessageType, NameAndId, ROLLMATCH, UNMATCH, ADDEMOTES } from 'befriendlier-shared'
import { DateTime } from 'luxon'

class Handler {
/**
* MAKE SURE TO CATCH ERRORS.
*/
public async rollMatch ({ userTwitch, channelTwitch }: ROLLMATCH) {
const { chatOwnerUser, profile } = await this.findProfileOrCreateByChatOwner(userTwitch, channelTwitch)

if (profile.rolls.length > 0) {
// Return match
const rau = await this.rollUntilAvailableUser(profile.rolls)

if (rau instanceof Error) {
throw this.error(MessageType.ERROR, userTwitch, channelTwitch,
'looks like you\'re not lucky today, rubber ducky 🦆 Try swiping again in a bit.')
} else {
const {
rolls,
user: matchUser,
profile: matchProfile,
} = rau

// We don't need to save the array if it's not been edited.
if (rolls instanceof Array && rolls.length !== profile.rolls.length) {
profile.rolls = rolls as number[]
await profile.save()
}

return { user: matchUser as User, profile: matchProfile as Profile }
}
}

if (profile.nextRolls.diffNow('hours').hours >= 0) {
throw this.error(MessageType.ERROR, userTwitch, channelTwitch, `you are on a cooldown. Please try again ${String(profile.nextRolls.toRelative())}.`)
}

await profile.preload('matches')

/**
* Find matches in the same chat,
* filter away deleted users,
* filter away own profile, mismatches and current matches.
*/
const profiles = await Profile.query()
.where('chatUserId', chatOwnerUser.id)
.where('enabled', true)
.whereNotIn('id', [
profile.id,
...profile.mismatches,
...profile.matches.map(match => match.id),
])

// Ratelimit user's rolls to every 5 hours.
profile.nextRolls = DateTime.fromJSDate(new Date()).plus({ hours: 5 })

if (profiles.length === 0) {
await profile.save()
throw this.error(MessageType.TAKEABREAK, userTwitch, channelTwitch,
`looks like you're not lucky today, rubber ducky 🦆 Try swiping again in ${String(profile.nextRolls.toRelative())}.`)
}

// Shuffle the array!
this.durstenfeldShuffle(profiles)

profile.rolls = profiles.map(profile => profile.id).slice(0, 10)

await profile.save()

const matchedUser = await this.findUserByProfile(profiles[0])

return { user: matchedUser, profile: profiles[0] }
}

/**
* MAKE SURE TO CATCH ERRORS.
*/
public async match ({ userTwitch, channelTwitch }: BASE) {
const { user, profile } = await this.findProfileOrCreateByChatOwner(userTwitch, channelTwitch)

const profileId = profile.rolls.shift()

const matchProfile = await Profile.find(profileId)

if (matchProfile === null) {
// Shouldn't hit here, maybe user deleted as soon as they tried to match.
Logger.error(`Tried to match with an unknown profile. profileId: ${String(profileId)}`)
throw this.error(MessageType.ERROR, userTwitch, channelTwitch, 'user does not exist.')
}

// Add match for this profile.
await profile.related('matches').attach({
[matchProfile.id]: {
user_id: profile.userId,
match_user_id: matchProfile.userId,
},
})

// Check if matched profile also has this user as a match, if so, announce match to both users.
const hasMatched = await Database.query().from('matches_lists').where({
profile_id: matchProfile.id,
match_user_id: user.id,
}).first()

const matchUser = await this.findUserByProfile(matchProfile)

if (matchUser === null) {
// Shouldn't hit here, maybe user deleted as soon as they tried to match.
Logger.error(`Tried to match with an unknown user. userId:${matchProfile.userId}, profileId: ${String(profileId)}`)
throw this.error(MessageType.ERROR, userTwitch, channelTwitch, 'user does not exist.')
}

await profile.save()

if (hasMatched !== null) {
return { attempt: MessageType.SUCCESS, user, matchUser }
} else {
return { attempt: MessageType.MATCH }
}
}

/**
* MAKE SURE TO CATCH ERRORS.
*/
public async unmatch ({ userTwitch, matchUserTwitch, channelTwitch }: UNMATCH) {
const { profile, chatOwnerUser } = await this.findProfileOrCreateByChatOwner(userTwitch, channelTwitch)

const matchUser = await User.findBy('name', matchUserTwitch.name)

if (matchUser === null) {
// Shouldn't hit here, maybe user deleted as soon as they tried to unmatch or they simply do not exist.
Logger.error(`Tried to unmatch with an unknown user. matchUserTwitch.name:${String(matchUserTwitch.name)}, profile.id: ${String(profile.id)}`)
throw this.error(MessageType.ERROR, userTwitch, channelTwitch, 'user does not exist.')
}

const matchProfile = await matchUser.related('profile').query().where('chatUserId', chatOwnerUser.id).first()

if (matchProfile === null) {
/**
* Shouldn't hit here, maybe user deleted their profile
* as soon as they tried to unmatch or it simply does not exist.
*/
Logger.error(`Tried to unmatch with an unknown profile. matchUserTwitch.name:${String(matchUserTwitch.name)}, profile.id: ${String(profile.id)}`)
throw this.error(MessageType.ERROR, userTwitch, channelTwitch, 'user does not exist.')
}

const match = await profile.related('matches').query().where('id', matchProfile.id).first()

if (match === null) {
return false
}

profile.mismatches.push(matchProfile.id)

await profile.save()

await profile.related('matches').detach([matchProfile.id])

return true
}

public async mismatch ({ userTwitch, channelTwitch }: BASE) {
const { profile } = await this.findProfileOrCreateByChatOwner(userTwitch, channelTwitch)

const profileId = profile.rolls.shift()

if (profileId === undefined) {
Logger.error(`Could not find rolls to mismatch. userTwitch.id:${String(userTwitch.id)}, profile.id: ${String(profile.id)}`)
throw this.error(
MessageType.ERROR, userTwitch, channelTwitch, 'looks like you\'re not currently matching with anyone.')
}

profile.mismatches.push(profileId)

await profile.save()
}

public async addEmotes ({ userTwitch, channelTwitch, emotes }: ADDEMOTES) {
const { profile } = await this.findProfileOrCreateByChatOwner(userTwitch, channelTwitch)

profile.favoriteEmotes = emotes

await profile.save()
}

private async findUserByProfile (profile: Profile) {
const user = await User.find(profile.userId)

if (user === null) {
// Shouldn't hit this at all.
Logger.error(`Tried to find a profile's user. profile.id: ${profile.id}, profile.userId: ${profile.userId}`)
// Ignore request.
throw new Error()
}

return user
}

private async findProfileOrCreateByChatOwner (user: User | NameAndId, channel: User | NameAndId):
Promise<{ user: User, chatOwnerUser: User, profile: Profile }> {
const userModel = user instanceof User ? user : await User.findBy('twitchID', user.id)
const chatOwnerUserModel = channel instanceof User ? channel : await User.findBy('twitchID', channel.id)
let profileModel: Profile | null

if (userModel === null) {
throw this.error(MessageType.UNREGISTERED, user, channel)
}

if (chatOwnerUserModel === null) {
// If this is hit, chat owner might've deleted their user. TODO: HANDLE
Logger.error(`Tried to find an unknown chat owner user. channelId: ${channel instanceof User ? String(channel.id) : String(channel.name)}`)
// Ignore request.
throw new Error()
}

profileModel = await userModel.related('profile').query().where('chatUserId', chatOwnerUserModel.id).first()

if (profileModel === null) {
// Register this profile for the user & continue.
profileModel = await userModel.related('profile').create({
chatUserId: chatOwnerUserModel.id,
rolls: [],
mismatches: [],
nextRolls: DateTime.fromJSDate(new Date()),
})
}

return { user: userModel, chatOwnerUser: chatOwnerUserModel, profile: profileModel }
}

private async rollUntilAvailableUser (rolls: number[]) {
if (rolls.length === 0) {
return new Error()
}

const profile = await Profile.find(rolls[0])
if (profile === null) {
// User probably deleted, reroll again.
rolls.shift()
return this.rollUntilAvailableUser(rolls)
}

const user = await this.findUserByProfile(profile)

return { rolls, user, profile }
}

private error (type: MessageType, user: User | NameAndId, channel: User | NameAndId, message?: string) {
const error = new Error(type)

;(error as any).data = {}

if (user instanceof User) {
(error as any).data.userTwitch = { id: user.twitchID, name: user.name }
} else {
(error as any).data.userTwitch = { id: user.id, name: user.name }
}

if (channel instanceof User) {
(error as any).data.channelTwitch = { id: channel.twitchID, name: channel.name }
} else {
(error as any).data.channelTwitch = { id: channel.id, name: channel.name }
}

if (message !== undefined) {
(error as any).data.result = { value: message }
}

return error
}

// https://stackoverflow.com/a/12646864
private durstenfeldShuffle (array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]
}
}
}

/**
* This makes our service a singleton
*/
export default new Handler()
Loading

0 comments on commit 37ee515

Please sign in to comment.