mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2026-08-31 19:03:45 +02:00
Add support for more limits
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash'
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { assertIdWithinFamily } from '../util/token'
|
||||
import { AppLogicAction } from './basetypes'
|
||||
|
||||
@@ -25,15 +26,21 @@ export class AddUsedTimeActionVersion2 extends AppLogicAction {
|
||||
readonly categoryId: string
|
||||
readonly timeToAdd: number
|
||||
readonly extraTimeToSubtract: number
|
||||
readonly additionalCountingSlots: Array<AddUsedTimeActionItemAdditionalCountingSlot>
|
||||
readonly sessionDurationLimits: Array<AddUsedTimeActionItemSessionDurationLimitSlot>
|
||||
}>
|
||||
readonly trustedTimestamp: number
|
||||
|
||||
constructor ({ dayOfEpoch, items }: {
|
||||
constructor ({ dayOfEpoch, items, trustedTimestamp }: {
|
||||
dayOfEpoch: number
|
||||
items: Array<{
|
||||
categoryId: string
|
||||
timeToAdd: number
|
||||
extraTimeToSubtract: number
|
||||
additionalCountingSlots: Array<AddUsedTimeActionItemAdditionalCountingSlot>
|
||||
sessionDurationLimits: Array<AddUsedTimeActionItemSessionDurationLimitSlot>
|
||||
}>
|
||||
trustedTimestamp: number
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -41,6 +48,10 @@ export class AddUsedTimeActionVersion2 extends AppLogicAction {
|
||||
throw new Error('illegal dayOfEpoch')
|
||||
}
|
||||
|
||||
if (trustedTimestamp < 0 || (!Number.isSafeInteger(trustedTimestamp))) {
|
||||
throw new Error('illegal trustedTimestamp')
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('missing items')
|
||||
}
|
||||
@@ -59,10 +70,25 @@ export class AddUsedTimeActionVersion2 extends AppLogicAction {
|
||||
if (item.extraTimeToSubtract < 0 || (!Number.isSafeInteger(item.extraTimeToSubtract))) {
|
||||
throw new Error('illegal extra time to subtract')
|
||||
}
|
||||
|
||||
if (
|
||||
uniq(item.additionalCountingSlots.map((item) => JSON.stringify(item.serialize()))).length !==
|
||||
item.additionalCountingSlots.length
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (
|
||||
uniq(item.sessionDurationLimits.map((item) => JSON.stringify(item.serialize()))).length !==
|
||||
item.sessionDurationLimits.length
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
})
|
||||
|
||||
this.dayOfEpoch = dayOfEpoch
|
||||
this.items = items
|
||||
this.trustedTimestamp = trustedTimestamp
|
||||
}
|
||||
|
||||
serialize = (): SerializedAddUsedTimeActionVersion2 => ({
|
||||
@@ -72,21 +98,84 @@ export class AddUsedTimeActionVersion2 extends AppLogicAction {
|
||||
categoryId: item.categoryId,
|
||||
tta: item.timeToAdd,
|
||||
etts: item.extraTimeToSubtract
|
||||
}))
|
||||
})),
|
||||
t: this.trustedTimestamp
|
||||
})
|
||||
|
||||
static parse = ({ d, i }: SerializedAddUsedTimeActionVersion2) => (
|
||||
static parse = ({ d, i, t }: SerializedAddUsedTimeActionVersion2) => (
|
||||
new AddUsedTimeActionVersion2({
|
||||
dayOfEpoch: d,
|
||||
items: i.map((item) => ({
|
||||
categoryId: item.categoryId,
|
||||
timeToAdd: item.tta,
|
||||
extraTimeToSubtract: item.etts
|
||||
}))
|
||||
extraTimeToSubtract: item.etts,
|
||||
sessionDurationLimits: (item.sdl ?? []).map((item) => AddUsedTimeActionItemSessionDurationLimitSlot.parse(item)),
|
||||
additionalCountingSlots: (item.as ?? []).map((item) => AddUsedTimeActionItemAdditionalCountingSlot.parse(item))
|
||||
})),
|
||||
trustedTimestamp: t ?? 0
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
class AddUsedTimeActionItemAdditionalCountingSlot {
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
|
||||
constructor ({ start, end }: { start: number, end: number }) {
|
||||
if ((!Number.isSafeInteger(start)) || (!Number.isSafeInteger(end))) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (start === MinuteOfDay.MIN && end === MinuteOfDay.MAX) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
this.start = start
|
||||
this.end = end
|
||||
}
|
||||
|
||||
serialize = () => [ this.start, this.end ]
|
||||
|
||||
static parse = ([ start, end ]: [number, number]) => new AddUsedTimeActionItemAdditionalCountingSlot({ start, end })
|
||||
}
|
||||
|
||||
class AddUsedTimeActionItemSessionDurationLimitSlot {
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
readonly duration: number
|
||||
readonly pause: number
|
||||
|
||||
constructor ({ start, end, duration, pause }: { start: number, end: number, duration: number, pause: number }) {
|
||||
if (
|
||||
(!Number.isSafeInteger(start)) || (!Number.isSafeInteger(end)) ||
|
||||
(!Number.isSafeInteger(duration)) || (!Number.isSafeInteger(pause))
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (duration <= 0 || pause <= 0) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.duration = duration
|
||||
this.pause = pause
|
||||
}
|
||||
|
||||
serialize = () => [ this.start, this.end ]
|
||||
|
||||
static parse = ([ start, end, duration, pause ]: [number, number, number, number]) => new AddUsedTimeActionItemSessionDurationLimitSlot({ start, end, duration, pause })
|
||||
}
|
||||
|
||||
export interface SerializedAddUsedTimeActionVersion2 {
|
||||
type: 'ADD_USED_TIME_V2'
|
||||
d: number
|
||||
@@ -94,5 +183,10 @@ export interface SerializedAddUsedTimeActionVersion2 {
|
||||
categoryId: string
|
||||
tta: number
|
||||
etts: number
|
||||
// start, end
|
||||
as?: Array<[number, number]>
|
||||
// start, end, length, pause
|
||||
sdl?: Array<[number, number, number, number]>
|
||||
}>
|
||||
t?: number
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -15,6 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { assertIdWithinFamily } from '../util/token'
|
||||
import { ParentAction } from './basetypes'
|
||||
|
||||
@@ -23,12 +24,23 @@ export class UpdateTimelimitRuleAction extends ParentAction {
|
||||
readonly maximumTimeInMillis: number
|
||||
readonly dayMask: number
|
||||
readonly applyToExtraTimeUsage: boolean
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
readonly sessionDurationMilliseconds: number
|
||||
readonly sessionPauseMilliseconds: number
|
||||
|
||||
constructor ({ ruleId, maximumTimeInMillis, dayMask, applyToExtraTimeUsage }: {
|
||||
constructor ({
|
||||
ruleId, maximumTimeInMillis, dayMask, applyToExtraTimeUsage,
|
||||
start, end, sessionDurationMilliseconds, sessionPauseMilliseconds
|
||||
}: {
|
||||
ruleId: string
|
||||
maximumTimeInMillis: number
|
||||
dayMask: number
|
||||
applyToExtraTimeUsage: boolean
|
||||
start: number
|
||||
end: number
|
||||
sessionDurationMilliseconds: number
|
||||
sessionPauseMilliseconds: number
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -36,6 +48,10 @@ export class UpdateTimelimitRuleAction extends ParentAction {
|
||||
this.maximumTimeInMillis = maximumTimeInMillis
|
||||
this.dayMask = dayMask
|
||||
this.applyToExtraTimeUsage = applyToExtraTimeUsage
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.sessionDurationMilliseconds = sessionDurationMilliseconds
|
||||
this.sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||
|
||||
assertIdWithinFamily(ruleId)
|
||||
|
||||
@@ -50,6 +66,23 @@ export class UpdateTimelimitRuleAction extends ParentAction {
|
||||
)) {
|
||||
throw new Error('invalid day mask')
|
||||
}
|
||||
|
||||
if (
|
||||
(!Number.isSafeInteger(start)) ||
|
||||
(!Number.isSafeInteger(end)) ||
|
||||
(!Number.isSafeInteger(sessionDurationMilliseconds)) ||
|
||||
(!Number.isSafeInteger(sessionPauseMilliseconds))
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) {
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
serialize = (): SerializedUpdateTimelimitRuleAction => ({
|
||||
@@ -57,15 +90,23 @@ export class UpdateTimelimitRuleAction extends ParentAction {
|
||||
ruleId: this.ruleId,
|
||||
time: this.maximumTimeInMillis,
|
||||
days: this.dayMask,
|
||||
extraTime: this.applyToExtraTimeUsage
|
||||
extraTime: this.applyToExtraTimeUsage,
|
||||
start: this.start,
|
||||
end: this.end,
|
||||
pause: this.sessionPauseMilliseconds,
|
||||
dur: this.sessionDurationMilliseconds
|
||||
})
|
||||
|
||||
static parse = ({ ruleId, time, days, extraTime }: SerializedUpdateTimelimitRuleAction) => (
|
||||
static parse = ({ ruleId, time, days, extraTime, start, end, dur, pause }: SerializedUpdateTimelimitRuleAction) => (
|
||||
new UpdateTimelimitRuleAction({
|
||||
ruleId,
|
||||
maximumTimeInMillis: time,
|
||||
dayMask: days,
|
||||
applyToExtraTimeUsage: extraTime
|
||||
applyToExtraTimeUsage: extraTime,
|
||||
start: start ?? MinuteOfDay.MIN,
|
||||
end: end ?? MinuteOfDay.MAX,
|
||||
sessionDurationMilliseconds: dur ?? 0,
|
||||
sessionPauseMilliseconds: pause ?? 0
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -76,4 +117,8 @@ export interface SerializedUpdateTimelimitRuleAction {
|
||||
time: number
|
||||
days: number
|
||||
extraTime: boolean
|
||||
start?: number
|
||||
end?: number
|
||||
dur?: number
|
||||
pause?: number
|
||||
}
|
||||
|
||||
+162
-1
@@ -24,6 +24,9 @@ const definitions = {
|
||||
},
|
||||
"users": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientLevel": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -252,6 +255,18 @@ const definitions = {
|
||||
},
|
||||
"extraTime": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"start": {
|
||||
"type": "number"
|
||||
},
|
||||
"end": {
|
||||
"type": "number"
|
||||
},
|
||||
"dur": {
|
||||
"type": "number"
|
||||
},
|
||||
"pause": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1087,6 +1102,18 @@ const definitions = {
|
||||
},
|
||||
"extraTime": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"start": {
|
||||
"type": "number"
|
||||
},
|
||||
"end": {
|
||||
"type": "number"
|
||||
},
|
||||
"dur": {
|
||||
"type": "number"
|
||||
},
|
||||
"pause": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1208,6 +1235,68 @@ const definitions = {
|
||||
},
|
||||
"etts": {
|
||||
"type": "number"
|
||||
},
|
||||
"as": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"additionalItems": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sdl": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"minItems": 4,
|
||||
"additionalItems": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1217,6 +1306,9 @@ const definitions = {
|
||||
"tta"
|
||||
]
|
||||
}
|
||||
},
|
||||
"t": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -1750,6 +1842,12 @@ const definitions = {
|
||||
"$ref": "#/definitions/ServerUsedTimeItem"
|
||||
}
|
||||
},
|
||||
"sessionDurations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ServerSessionDurationItem"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1757,6 +1855,7 @@ const definitions = {
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"categoryId",
|
||||
"sessionDurations",
|
||||
"times",
|
||||
"version"
|
||||
]
|
||||
@@ -1769,14 +1868,60 @@ const definitions = {
|
||||
},
|
||||
"time": {
|
||||
"type": "number"
|
||||
},
|
||||
"start": {
|
||||
"type": "number"
|
||||
},
|
||||
"end": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"day",
|
||||
"end",
|
||||
"start",
|
||||
"time"
|
||||
]
|
||||
},
|
||||
"ServerSessionDurationItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"md": {
|
||||
"description": "the maximum duration of a session (maxSessionDuration)",
|
||||
"type": "number"
|
||||
},
|
||||
"spd": {
|
||||
"description": "the pause duration after a session (sessionPauseDuration)",
|
||||
"type": "number"
|
||||
},
|
||||
"sm": {
|
||||
"description": "the start minute of the day of the session/ the rule\nwhich created this session (startMinuteOfDay)",
|
||||
"type": "number"
|
||||
},
|
||||
"em": {
|
||||
"description": "the end minute of the day of the session/ the rule\nwhich created this session (endMinuteOfDay)",
|
||||
"type": "number"
|
||||
},
|
||||
"l": {
|
||||
"description": "the timestamp of the last usage of this session (lastUsage)",
|
||||
"type": "number"
|
||||
},
|
||||
"d": {
|
||||
"description": "the duration of the last/ current session (lastSessionDuration)",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"d",
|
||||
"em",
|
||||
"l",
|
||||
"md",
|
||||
"sm",
|
||||
"spd"
|
||||
]
|
||||
},
|
||||
"ServerUpdatedTimeLimitRules": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1814,14 +1959,30 @@ const definitions = {
|
||||
},
|
||||
"maxTime": {
|
||||
"type": "number"
|
||||
},
|
||||
"start": {
|
||||
"type": "number"
|
||||
},
|
||||
"end": {
|
||||
"type": "number"
|
||||
},
|
||||
"session": {
|
||||
"type": "number"
|
||||
},
|
||||
"pause": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"dayMask",
|
||||
"end",
|
||||
"extraTime",
|
||||
"id",
|
||||
"maxTime"
|
||||
"maxTime",
|
||||
"pause",
|
||||
"session",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"ServerUserList": {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogi
|
||||
import { createUmzug } from './migration/umzug'
|
||||
import { createOldDeviceModel, OldDeviceModelStatic } from './olddevice'
|
||||
import { createPurchaseModel, PurchaseModelStatic } from './purchase'
|
||||
import { createSessionDurationModel, SessionDurationModelStatic } from './sessionduration'
|
||||
import { createTimelimitRuleModel, TimelimitRuleModelStatic } from './timelimitrule'
|
||||
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
|
||||
import { createUserModel, UserModelStatic } from './user'
|
||||
@@ -46,6 +47,7 @@ export interface Database {
|
||||
mailLoginToken: MailLoginTokenModelStatic
|
||||
oldDevice: OldDeviceModelStatic
|
||||
purchase: PurchaseModelStatic
|
||||
sessionDuration: SessionDurationModelStatic
|
||||
timelimitRule: TimelimitRuleModelStatic
|
||||
usedTime: UsedTimeModelStatic
|
||||
user: UserModelStatic
|
||||
@@ -65,6 +67,7 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
|
||||
mailLoginToken: createMailLoginTokenModel(sequelize),
|
||||
oldDevice: createOldDeviceModel(sequelize),
|
||||
purchase: createPurchaseModel(sequelize),
|
||||
sessionDuration: createSessionDurationModel(sequelize),
|
||||
timelimitRule: createTimelimitRuleModel(sequelize),
|
||||
usedTime: createUsedTimeModel(sequelize),
|
||||
user: createUserModel(sequelize),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -24,7 +24,7 @@ import { attributes as categoryAppAttributes } from '../../categoryapp'
|
||||
import { attributesVersion1 as deviceAttributes } from '../../device'
|
||||
import { attributes as familyAttributes } from '../../family'
|
||||
import { attributes as purchaseAttributes } from '../../purchase'
|
||||
import { attributes as timelimitruleAttributes } from '../../timelimitrule'
|
||||
import { attributesVersion1 as timelimitruleAttributes } from '../../timelimitrule'
|
||||
import { attributesVersion1 as usedTimeAttribute } from '../../usedtime'
|
||||
import { attributesVersion1 as userAttributes } from '../../user'
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
|
||||
import { MinuteOfDay } from '../../../util/minuteofday'
|
||||
import { attributesVersion1 as sessionDurationAttributes } from '../../sessionduration'
|
||||
import { attributesVersion2 as timelimitRuleAttributes } from '../../timelimitrule'
|
||||
import {
|
||||
attributesVersion1 as usedTimeAttributesVersion1,
|
||||
attributesVersion2 as usedTimeAttributesVersion2,
|
||||
attributesVersion3 as usedTimeAttributesVersion3
|
||||
} from '../../usedtime'
|
||||
|
||||
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
|
||||
await sequelize.transaction({
|
||||
type: Transaction.TYPES.EXCLUSIVE
|
||||
}, async (transaction) => {
|
||||
// session durations
|
||||
await queryInterface.createTable('SessionDurations', sessionDurationAttributes, { transaction })
|
||||
|
||||
// timelimit rule table
|
||||
await queryInterface.addColumn('TimelimitRules', 'startMinuteOfDay', {
|
||||
...timelimitRuleAttributes.startMinuteOfDay
|
||||
}, { transaction })
|
||||
|
||||
await queryInterface.addColumn('TimelimitRules', 'endMinuteOfDay', {
|
||||
...timelimitRuleAttributes.endMinuteOfDay
|
||||
}, { transaction })
|
||||
|
||||
await queryInterface.addColumn('TimelimitRules', 'sessionDurationMilliseconds', {
|
||||
...timelimitRuleAttributes.sessionDurationMilliseconds
|
||||
}, { transaction })
|
||||
|
||||
await queryInterface.addColumn('TimelimitRules', 'sessionPauseMilliseconds', {
|
||||
...timelimitRuleAttributes.sessionPauseMilliseconds
|
||||
}, { transaction })
|
||||
|
||||
// used times
|
||||
await queryInterface.renameTable('UsedTimes', 'UsedTimesOld', { transaction })
|
||||
|
||||
await queryInterface.createTable('UsedTimes', {
|
||||
...usedTimeAttributesVersion1,
|
||||
...usedTimeAttributesVersion2,
|
||||
...usedTimeAttributesVersion3
|
||||
}, { transaction })
|
||||
|
||||
await sequelize.query(`
|
||||
INSERT INTO UsedTimes (familyId, categoryId, dayOfEpoch, usedTime, lastUpdate, startMinuteOfDay, endMinuteOfDay)
|
||||
SELECT familyId, categoryId, dayOfEpoch, usedTime, lastUpdate,
|
||||
${MinuteOfDay.MIN} AS startMinuteOfDay, ${MinuteOfDay.MAX} AS endMinuteOfDay
|
||||
FROM UsedTimesOld
|
||||
`, { transaction })
|
||||
|
||||
await queryInterface.dropTable('UsedTimesOld', { transaction })
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
|
||||
await sequelize.transaction({
|
||||
type: Transaction.TYPES.EXCLUSIVE
|
||||
}, async (transaction) => {
|
||||
// session durations
|
||||
await queryInterface.dropTable('SessionDurations', { transaction })
|
||||
|
||||
// timelimit rule table
|
||||
await queryInterface.removeColumn('TimelimitRules', 'startMinuteOfDay', { transaction })
|
||||
await queryInterface.removeColumn('TimelimitRules', 'endMinuteOfDay', { transaction })
|
||||
await queryInterface.removeColumn('TimelimitRules', 'sessionDurationMilliseconds', { transaction })
|
||||
await queryInterface.removeColumn('TimelimitRules', 'sessionPauseMilliseconds', { transaction })
|
||||
|
||||
// used times
|
||||
throw new Error('not implemented')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { familyIdColumn, idWithinFamilyColumn, timestampColumn } from './columns'
|
||||
import { SequelizeAttributes } from './types'
|
||||
|
||||
interface SessionDurationAttributesVersion1 {
|
||||
familyId: string
|
||||
categoryId: string
|
||||
maxSessionDuration: number
|
||||
sessionPauseDuration: number
|
||||
startMinuteOfDay: number
|
||||
endMinuteOfDay: number
|
||||
lastUsage: string
|
||||
lastSessionDuration: number
|
||||
// used for deleting old items, set by the server
|
||||
roundedLastUpdate: string
|
||||
}
|
||||
|
||||
export type SessionDurationAttributes = SessionDurationAttributesVersion1
|
||||
|
||||
export type SessionDurationModel = Sequelize.Model & SessionDurationAttributes
|
||||
export type SessionDurationModelStatic = typeof Sequelize.Model & {
|
||||
new (values?: object, options?: Sequelize.BuildOptions): SessionDurationModel;
|
||||
}
|
||||
|
||||
export const attributesVersion1: SequelizeAttributes<SessionDurationAttributesVersion1> = {
|
||||
familyId: {
|
||||
...familyIdColumn,
|
||||
primaryKey: true
|
||||
},
|
||||
categoryId: {
|
||||
...idWithinFamilyColumn,
|
||||
primaryKey: true
|
||||
},
|
||||
maxSessionDuration: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
min: 1
|
||||
}
|
||||
},
|
||||
sessionPauseDuration: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
min: 1
|
||||
}
|
||||
},
|
||||
startMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX
|
||||
}
|
||||
},
|
||||
endMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX,
|
||||
customValidator (endMinuteOfDay: unknown) {
|
||||
const startMinuteOfDay = this.startMinuteOfDay
|
||||
|
||||
if (typeof endMinuteOfDay !== 'number' || typeof startMinuteOfDay !== 'number') {
|
||||
throw new Error('wrong data types')
|
||||
}
|
||||
|
||||
if (startMinuteOfDay > endMinuteOfDay) {
|
||||
throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
lastUsage: { ...timestampColumn },
|
||||
lastSessionDuration: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
min: 0
|
||||
}
|
||||
},
|
||||
roundedLastUpdate: { ...timestampColumn }
|
||||
}
|
||||
|
||||
export const attributes: SequelizeAttributes<SessionDurationAttributes> = {
|
||||
...attributesVersion1
|
||||
}
|
||||
|
||||
export const createSessionDurationModel = (sequelize: Sequelize.Sequelize): SessionDurationModelStatic => sequelize.define('SessionDuration', attributes) as SessionDurationModelStatic
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -16,10 +16,11 @@
|
||||
*/
|
||||
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { booleanColumn, familyIdColumn, idWithinFamilyColumn } from './columns'
|
||||
import { SequelizeAttributes } from './types'
|
||||
|
||||
export interface TimelimitRuleAttributes {
|
||||
interface TimelimitRuleAttributesVersion1 {
|
||||
familyId: string
|
||||
ruleId: string
|
||||
categoryId: string
|
||||
@@ -28,12 +29,21 @@ export interface TimelimitRuleAttributes {
|
||||
maximumTimeInMillis: number
|
||||
}
|
||||
|
||||
interface TimelimitRuleAttributesVersion2 {
|
||||
startMinuteOfDay: number
|
||||
endMinuteOfDay: number
|
||||
sessionDurationMilliseconds: number
|
||||
sessionPauseMilliseconds: number
|
||||
}
|
||||
|
||||
type TimelimitRuleAttributes = TimelimitRuleAttributesVersion1 & TimelimitRuleAttributesVersion2
|
||||
|
||||
export type TimelimitRuleModel = Sequelize.Model & TimelimitRuleAttributes
|
||||
export type TimelimitRuleModelStatic = typeof Sequelize.Model & {
|
||||
new (values?: object, options?: Sequelize.BuildOptions): TimelimitRuleModel;
|
||||
}
|
||||
|
||||
export const attributes: SequelizeAttributes<TimelimitRuleAttributes> = {
|
||||
export const attributesVersion1: SequelizeAttributes<TimelimitRuleAttributesVersion1> = {
|
||||
familyId: {
|
||||
...familyIdColumn,
|
||||
primaryKey: true
|
||||
@@ -61,4 +71,57 @@ export const attributes: SequelizeAttributes<TimelimitRuleAttributes> = {
|
||||
}
|
||||
}
|
||||
|
||||
export const attributesVersion2: SequelizeAttributes<TimelimitRuleAttributesVersion2> = {
|
||||
startMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX
|
||||
},
|
||||
allowNull: false,
|
||||
defaultValue: MinuteOfDay.MIN
|
||||
},
|
||||
endMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX,
|
||||
customValidator (endMinuteOfDay: unknown) {
|
||||
const startMinuteOfDay = this.startMinuteOfDay
|
||||
|
||||
if (typeof endMinuteOfDay !== 'number' || typeof startMinuteOfDay !== 'number') {
|
||||
throw new Error('wrong data types')
|
||||
}
|
||||
|
||||
if (startMinuteOfDay > endMinuteOfDay) {
|
||||
throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay')
|
||||
}
|
||||
}
|
||||
},
|
||||
allowNull: false,
|
||||
defaultValue: MinuteOfDay.MAX
|
||||
},
|
||||
sessionDurationMilliseconds: {
|
||||
type: Sequelize.INTEGER,
|
||||
validate: {
|
||||
min: 0
|
||||
},
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
sessionPauseMilliseconds: {
|
||||
type: Sequelize.INTEGER,
|
||||
validate: {
|
||||
min: 0
|
||||
},
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
}
|
||||
|
||||
export const attributes: SequelizeAttributes<TimelimitRuleAttributes> = {
|
||||
...attributesVersion1,
|
||||
...attributesVersion2
|
||||
}
|
||||
|
||||
export const createTimelimitRuleModel = (sequelize: Sequelize.Sequelize): TimelimitRuleModelStatic => sequelize.define('TimelimitRule', attributes) as TimelimitRuleModelStatic
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { familyIdColumn, idWithinFamilyColumn, timestampColumn } from './columns'
|
||||
import { SequelizeAttributes } from './types'
|
||||
|
||||
@@ -30,7 +31,13 @@ export interface UsedTimeAttributesVersion2 {
|
||||
lastUpdate: string
|
||||
}
|
||||
|
||||
export type UsedTimeAttributes = UsedTimeAttributesVersion1 & UsedTimeAttributesVersion2
|
||||
export interface UsedTimeAttributesVersion3 {
|
||||
startMinuteOfDay: number
|
||||
endMinuteOfDay: number
|
||||
}
|
||||
|
||||
export type UsedTimeAttributes = UsedTimeAttributesVersion1 &
|
||||
UsedTimeAttributesVersion2 & UsedTimeAttributesVersion3
|
||||
|
||||
export type UsedTimeModel = Sequelize.Model & UsedTimeAttributes
|
||||
export type UsedTimeModelStatic = typeof Sequelize.Model & {
|
||||
@@ -70,9 +77,44 @@ export const attributesVersion2: SequelizeAttributes<UsedTimeAttributesVersion2>
|
||||
}
|
||||
}
|
||||
|
||||
export const attributes = {
|
||||
export const attributesVersion3: SequelizeAttributes<UsedTimeAttributesVersion3> = {
|
||||
startMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: MinuteOfDay.MIN,
|
||||
primaryKey: true,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX
|
||||
}
|
||||
},
|
||||
endMinuteOfDay: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: MinuteOfDay.MAX,
|
||||
primaryKey: true,
|
||||
validate: {
|
||||
min: MinuteOfDay.MIN,
|
||||
max: MinuteOfDay.MAX,
|
||||
customValidator (endMinuteOfDay: unknown) {
|
||||
const startMinuteOfDay = this.startMinuteOfDay
|
||||
|
||||
if (typeof endMinuteOfDay !== 'number' || typeof startMinuteOfDay !== 'number') {
|
||||
throw new Error('wrong data types')
|
||||
}
|
||||
|
||||
if (startMinuteOfDay > endMinuteOfDay) {
|
||||
throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attributes: SequelizeAttributes<UsedTimeAttributesVersion3> = {
|
||||
...attributesVersion1,
|
||||
...attributesVersion2
|
||||
...attributesVersion2,
|
||||
...attributesVersion3
|
||||
}
|
||||
|
||||
export const createUsedTimeModel = (sequelize: Sequelize.Sequelize): UsedTimeModelStatic => sequelize.define('UsedTime', attributes) as UsedTimeModelStatic
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { AddUsedTimeAction } from '../../../../action'
|
||||
import { MinuteOfDay } from '../../../../util/minuteofday'
|
||||
import { Cache } from '../cache'
|
||||
|
||||
export const getRoundedTimestamp = () => {
|
||||
@@ -68,7 +69,9 @@ export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
|
||||
where: {
|
||||
familyId: cache.familyId,
|
||||
categoryId: categoryId,
|
||||
dayOfEpoch: action.dayOfEpoch
|
||||
dayOfEpoch: action.dayOfEpoch,
|
||||
startMinuteOfDay: MinuteOfDay.MIN,
|
||||
endMinuteOfDay: MinuteOfDay.MAX
|
||||
},
|
||||
transaction: cache.transaction
|
||||
})
|
||||
@@ -80,7 +83,9 @@ export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
|
||||
categoryId: categoryId,
|
||||
dayOfEpoch: action.dayOfEpoch,
|
||||
usedTime: action.timeToAdd,
|
||||
lastUpdate: roundedTimestamp
|
||||
lastUpdate: roundedTimestamp,
|
||||
startMinuteOfDay: MinuteOfDay.MIN,
|
||||
endMinuteOfDay: MinuteOfDay.MAX
|
||||
}, {
|
||||
transaction: cache.transaction
|
||||
})
|
||||
|
||||
@@ -17,15 +17,23 @@
|
||||
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { AddUsedTimeActionVersion2 } from '../../../../action'
|
||||
import { MinuteOfDay } from '../../../../util/minuteofday'
|
||||
import { Cache } from '../cache'
|
||||
import { getRoundedTimestamp } from './addusedtime'
|
||||
import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime'
|
||||
|
||||
export const getRoundedTimestampForSessionDuration = () => {
|
||||
const now = Date.now()
|
||||
|
||||
return now - (now % (1000 * 60 * 60 * 12 /* 12 hours */))
|
||||
}
|
||||
|
||||
export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }: {
|
||||
deviceId: string
|
||||
action: AddUsedTimeActionVersion2
|
||||
cache: Cache
|
||||
}) {
|
||||
const roundedTimestamp = getRoundedTimestamp().toString(10)
|
||||
const roundedTimestampForUsedTime = getRoundedTimestampForUsedTime().toString(10)
|
||||
const roundedTimestampForSessionDuration = getRoundedTimestampForSessionDuration().toString(10)
|
||||
|
||||
for (let i = 0; i < action.items.length; i++) {
|
||||
const item = action.items[i]
|
||||
@@ -41,6 +49,7 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }:
|
||||
'extraTimeInMillis'
|
||||
]
|
||||
})
|
||||
|
||||
// verify that the category exists
|
||||
if (!categoryEntryUnsafe) {
|
||||
cache.requireFullSync()
|
||||
@@ -53,16 +62,19 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }:
|
||||
extraTimeInMillis: categoryEntryUnsafe.extraTimeInMillis
|
||||
}
|
||||
|
||||
if (item.timeToAdd !== 0) {
|
||||
// tslint:disable-next-line:no-inner-declarations
|
||||
async function handle (start: number, end: number) {
|
||||
// try to update first
|
||||
const [updatedRows] = await cache.database.usedTime.update({
|
||||
usedTime: Sequelize.literal(`usedTime + ${item.timeToAdd}`) as any,
|
||||
lastUpdate: roundedTimestamp
|
||||
lastUpdate: roundedTimestampForUsedTime
|
||||
}, {
|
||||
where: {
|
||||
familyId: cache.familyId,
|
||||
categoryId: item.categoryId,
|
||||
dayOfEpoch: action.dayOfEpoch
|
||||
dayOfEpoch: action.dayOfEpoch,
|
||||
startMinuteOfDay: start,
|
||||
endMinuteOfDay: end
|
||||
},
|
||||
transaction: cache.transaction
|
||||
})
|
||||
@@ -74,15 +86,75 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }:
|
||||
categoryId: item.categoryId,
|
||||
dayOfEpoch: action.dayOfEpoch,
|
||||
usedTime: item.timeToAdd,
|
||||
lastUpdate: roundedTimestamp
|
||||
lastUpdate: roundedTimestampForUsedTime,
|
||||
startMinuteOfDay: start,
|
||||
endMinuteOfDay: end
|
||||
}, {
|
||||
transaction: cache.transaction
|
||||
})
|
||||
}
|
||||
|
||||
cache.categoriesWithModifiedUsedTimes.push(item.categoryId)
|
||||
}
|
||||
|
||||
await handle(MinuteOfDay.MIN, MinuteOfDay.MAX)
|
||||
|
||||
for (let j = 0; j < item.additionalCountingSlots.length; j++) {
|
||||
const slot = item.additionalCountingSlots[j]
|
||||
|
||||
await handle(slot.start, slot.end)
|
||||
}
|
||||
|
||||
const hasTrustedTimestamp = action.trustedTimestamp !== 0
|
||||
|
||||
for (let j = 0; j < item.sessionDurationLimits.length; j++) {
|
||||
const limit = item.sessionDurationLimits[j]
|
||||
|
||||
const oldItem = await cache.database.sessionDuration.findOne({
|
||||
where: {
|
||||
familyId: cache.familyId,
|
||||
categoryId: item.categoryId,
|
||||
maxSessionDuration: limit.duration,
|
||||
sessionPauseDuration: limit.pause,
|
||||
startMinuteOfDay: limit.start,
|
||||
endMinuteOfDay: limit.end
|
||||
},
|
||||
transaction: cache.transaction
|
||||
})
|
||||
|
||||
if (oldItem) {
|
||||
if (hasTrustedTimestamp) {
|
||||
oldItem.lastUsage = action.trustedTimestamp.toString(10)
|
||||
}
|
||||
|
||||
if (
|
||||
hasTrustedTimestamp &&
|
||||
action.trustedTimestamp > parseInt(oldItem.lastUsage, 10) + oldItem.sessionPauseDuration
|
||||
) {
|
||||
oldItem.lastSessionDuration = item.timeToAdd
|
||||
} else {
|
||||
oldItem.lastSessionDuration = oldItem.lastSessionDuration + item.timeToAdd
|
||||
}
|
||||
|
||||
oldItem.roundedLastUpdate = roundedTimestampForSessionDuration
|
||||
|
||||
await oldItem.save({ transaction: cache.transaction })
|
||||
} else {
|
||||
await cache.database.sessionDuration.create({
|
||||
familyId: cache.familyId,
|
||||
categoryId: item.categoryId,
|
||||
maxSessionDuration: limit.duration,
|
||||
sessionPauseDuration: limit.pause,
|
||||
startMinuteOfDay: limit.start,
|
||||
endMinuteOfDay: limit.end,
|
||||
// end of primary key
|
||||
lastUsage: action.trustedTimestamp,
|
||||
lastSessionDuration: item.timeToAdd,
|
||||
roundedLastUpdate: roundedTimestampForSessionDuration
|
||||
}, { transaction: cache.transaction })
|
||||
}
|
||||
}
|
||||
|
||||
cache.categoriesWithModifiedUsedTimes.push(item.categoryId)
|
||||
|
||||
if (item.extraTimeToSubtract !== 0) {
|
||||
await cache.database.category.update({
|
||||
extraTimeInMillis: Math.max(0, categoryEntry.extraTimeInMillis - item.extraTimeToSubtract)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -34,7 +34,11 @@ export async function dispatchCreateTimeLimitRule ({ action, cache }: {
|
||||
categoryId: action.rule.categoryId,
|
||||
applyToExtraTimeUsage: action.rule.applyToExtraTimeUsage,
|
||||
maximumTimeInMillis: action.rule.maxTimeInMillis,
|
||||
dayMaskAsBitmask: action.rule.dayMask
|
||||
dayMaskAsBitmask: action.rule.dayMask,
|
||||
startMinuteOfDay: action.rule.start,
|
||||
endMinuteOfDay: action.rule.end,
|
||||
sessionDurationMilliseconds: action.rule.sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds: action.rule.sessionPauseMilliseconds
|
||||
}, { transaction: cache.transaction })
|
||||
|
||||
cache.categoriesWithModifiedTimeLimitRules.push(action.rule.categoryId)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -37,6 +37,10 @@ export async function dispatchUpdateTimelimitRule ({ action, cache }: {
|
||||
ruleEntry.applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
||||
ruleEntry.dayMaskAsBitmask = action.dayMask
|
||||
ruleEntry.maximumTimeInMillis = action.maximumTimeInMillis
|
||||
ruleEntry.startMinuteOfDay = action.start
|
||||
ruleEntry.endMinuteOfDay = action.end
|
||||
ruleEntry.sessionDurationMilliseconds = action.sessionDurationMilliseconds
|
||||
ruleEntry.sessionPauseMilliseconds = action.sessionPauseMilliseconds
|
||||
|
||||
await ruleEntry.save({ transaction: cache.transaction })
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
ServerUpdatedCategoryBaseData, ServerUpdatedCategoryUsedTimes,
|
||||
ServerUpdatedTimeLimitRules
|
||||
} from '../../object/serverdatastatus'
|
||||
import { MinuteOfDay } from '../../util/minuteofday'
|
||||
|
||||
export const generateServerDataStatus = async ({ database, clientStatus, familyId, transaction }: {
|
||||
database: Database,
|
||||
@@ -429,7 +430,11 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
|
||||
'categoryId',
|
||||
'applyToExtraTimeUsage',
|
||||
'maximumTimeInMillis',
|
||||
'dayMaskAsBitmask'
|
||||
'dayMaskAsBitmask',
|
||||
'startMinuteOfDay',
|
||||
'endMinuteOfDay',
|
||||
'sessionDurationMilliseconds',
|
||||
'sessionPauseMilliseconds'
|
||||
],
|
||||
transaction
|
||||
})).map((item) => ({
|
||||
@@ -437,7 +442,11 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
|
||||
categoryId: item.categoryId,
|
||||
applyToExtraTimeUsage: item.applyToExtraTimeUsage,
|
||||
maximumTimeInMillis: item.maximumTimeInMillis,
|
||||
dayMaskAsBitmask: item.dayMaskAsBitmask
|
||||
dayMaskAsBitmask: item.dayMaskAsBitmask,
|
||||
startMinuteOfDay: item.startMinuteOfDay,
|
||||
endMinuteOfDay: item.endMinuteOfDay,
|
||||
sessionDurationMilliseconds: item.sessionDurationMilliseconds,
|
||||
sessionPauseMilliseconds: item.sessionPauseMilliseconds
|
||||
}))
|
||||
|
||||
const getCategoryRulesVersion = (categoryId: string) => {
|
||||
@@ -456,26 +465,65 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
|
||||
id: item.ruleId,
|
||||
extraTime: item.applyToExtraTimeUsage,
|
||||
dayMask: item.dayMaskAsBitmask,
|
||||
maxTime: item.maximumTimeInMillis
|
||||
maxTime: item.maximumTimeInMillis,
|
||||
start: item.startMinuteOfDay,
|
||||
end: item.endMinuteOfDay,
|
||||
session: item.sessionDurationMilliseconds,
|
||||
pause: item.sessionPauseMilliseconds
|
||||
})),
|
||||
version: getCategoryRulesVersion(categoryId)
|
||||
}))
|
||||
}
|
||||
|
||||
if (categoryIdsToSyncUsedTimes.length > 0) {
|
||||
const dataForSyncing = (await database.usedTime.findAll({
|
||||
const usedTimesForSyncing = (await database.usedTime.findAll({
|
||||
where: {
|
||||
familyId,
|
||||
categoryId: {
|
||||
[Sequelize.Op.in]: categoryIdsToSyncUsedTimes
|
||||
},
|
||||
...(clientStatus.clientLevel === undefined || clientStatus.clientLevel < 2) ? {
|
||||
startMinuteOfDay: MinuteOfDay.MIN,
|
||||
endMinuteOfDay: MinuteOfDay.MAX
|
||||
} : {}
|
||||
},
|
||||
attributes: [
|
||||
'categoryId', 'dayOfEpoch', 'usedTime', 'startMinuteOfDay', 'endMinuteOfDay'
|
||||
],
|
||||
transaction
|
||||
})).map((item) => ({
|
||||
categoryId: item.categoryId,
|
||||
dayOfEpoch: item.dayOfEpoch,
|
||||
usedTime: item.usedTime,
|
||||
startMinuteOfDay: item.startMinuteOfDay,
|
||||
endMinuteOfDay: item.endMinuteOfDay
|
||||
}))
|
||||
|
||||
const sessionDurationsForSyncing = (await database.sessionDuration.findAll({
|
||||
where: {
|
||||
familyId,
|
||||
categoryId: {
|
||||
[Sequelize.Op.in]: categoryIdsToSyncUsedTimes
|
||||
}
|
||||
},
|
||||
attributes: ['categoryId', 'dayOfEpoch', 'usedTime'],
|
||||
attributes: [
|
||||
'categoryId',
|
||||
'maxSessionDuration',
|
||||
'sessionPauseDuration',
|
||||
'startMinuteOfDay',
|
||||
'endMinuteOfDay',
|
||||
'lastUsage',
|
||||
'lastSessionDuration'
|
||||
],
|
||||
transaction
|
||||
})).map((item) => ({
|
||||
categoryId: item.categoryId,
|
||||
dayOfEpoch: item.dayOfEpoch,
|
||||
usedTime: item.usedTime
|
||||
maxSessionDuration: item.maxSessionDuration,
|
||||
sessionPauseDuration: item.sessionPauseDuration,
|
||||
startMinuteOfDay: item.startMinuteOfDay,
|
||||
endMinuteOfDay: item.endMinuteOfDay,
|
||||
lastUsage: item.lastUsage,
|
||||
lastSessionDuration: item.lastSessionDuration
|
||||
}))
|
||||
|
||||
const getCategoryUsedTimesVersion = (categoryId: string) => {
|
||||
@@ -490,9 +538,19 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
|
||||
|
||||
result.usedTimes = categoryIdsToSyncUsedTimes.map((categoryId): ServerUpdatedCategoryUsedTimes => ({
|
||||
categoryId,
|
||||
times: dataForSyncing.filter((item) => item.categoryId === categoryId).map((item) => ({
|
||||
times: usedTimesForSyncing.filter((item) => item.categoryId === categoryId).map((item) => ({
|
||||
day: item.dayOfEpoch,
|
||||
time: item.usedTime
|
||||
time: item.usedTime,
|
||||
start: item.startMinuteOfDay,
|
||||
end: item.endMinuteOfDay
|
||||
})),
|
||||
sessionDurations: sessionDurationsForSyncing.filter((item) => item.categoryId === categoryId).map((item) => ({
|
||||
md: item.maxSessionDuration,
|
||||
spd: item.sessionPauseDuration,
|
||||
sm: item.startMinuteOfDay,
|
||||
em: item.endMinuteOfDay,
|
||||
l: parseInt(item.lastUsage, 10),
|
||||
d: item.lastSessionDuration
|
||||
})),
|
||||
version: getCategoryUsedTimesVersion(categoryId)
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -15,6 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MinuteOfDay } from '../util/minuteofday'
|
||||
import { assertIdWithinFamily } from '../util/token'
|
||||
|
||||
export class TimelimitRule {
|
||||
@@ -23,19 +24,34 @@ export class TimelimitRule {
|
||||
readonly maxTimeInMillis: number
|
||||
readonly dayMask: number // stored as bitmask
|
||||
readonly applyToExtraTimeUsage: boolean
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
readonly sessionDurationMilliseconds: number
|
||||
readonly sessionPauseMilliseconds: number
|
||||
|
||||
constructor ({ ruleId, categoryId, maxTimeInMillis, dayMask, applyToExtraTimeUsage }: {
|
||||
constructor ({
|
||||
ruleId, categoryId, maxTimeInMillis, dayMask, applyToExtraTimeUsage,
|
||||
start, end, sessionDurationMilliseconds, sessionPauseMilliseconds
|
||||
}: {
|
||||
ruleId: string
|
||||
categoryId: string
|
||||
maxTimeInMillis: number
|
||||
dayMask: number
|
||||
applyToExtraTimeUsage: boolean
|
||||
start: number
|
||||
end: number
|
||||
sessionDurationMilliseconds: number
|
||||
sessionPauseMilliseconds: number
|
||||
}) {
|
||||
this.ruleId = ruleId
|
||||
this.categoryId = categoryId
|
||||
this.maxTimeInMillis = maxTimeInMillis
|
||||
this.dayMask = dayMask
|
||||
this.applyToExtraTimeUsage = applyToExtraTimeUsage
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.sessionDurationMilliseconds = sessionDurationMilliseconds
|
||||
this.sessionPauseMilliseconds = sessionPauseMilliseconds
|
||||
|
||||
assertIdWithinFamily(ruleId)
|
||||
assertIdWithinFamily(categoryId)
|
||||
@@ -51,6 +67,23 @@ export class TimelimitRule {
|
||||
)) {
|
||||
throw new Error('invalid day mask')
|
||||
}
|
||||
|
||||
if (
|
||||
(!Number.isSafeInteger(start)) ||
|
||||
(!Number.isSafeInteger(end)) ||
|
||||
(!Number.isSafeInteger(sessionDurationMilliseconds)) ||
|
||||
(!Number.isSafeInteger(sessionPauseMilliseconds))
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) {
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
serialize = (): SerializedTimeLimitRule => ({
|
||||
@@ -58,16 +91,24 @@ export class TimelimitRule {
|
||||
categoryId: this.categoryId,
|
||||
time: this.maxTimeInMillis,
|
||||
days: this.dayMask,
|
||||
extraTime: this.applyToExtraTimeUsage
|
||||
extraTime: this.applyToExtraTimeUsage,
|
||||
start: this.start,
|
||||
end: this.end,
|
||||
pause: this.sessionPauseMilliseconds,
|
||||
dur: this.sessionDurationMilliseconds
|
||||
})
|
||||
|
||||
static parse = ({ ruleId, categoryId, time, days, extraTime }: SerializedTimeLimitRule) => (
|
||||
static parse = ({ ruleId, categoryId, time, days, extraTime, start, end, dur, pause }: SerializedTimeLimitRule) => (
|
||||
new TimelimitRule({
|
||||
ruleId,
|
||||
categoryId,
|
||||
maxTimeInMillis: time,
|
||||
dayMask: days,
|
||||
applyToExtraTimeUsage: extraTime
|
||||
applyToExtraTimeUsage: extraTime,
|
||||
start: start ?? MinuteOfDay.MIN,
|
||||
end: end ?? MinuteOfDay.MAX,
|
||||
sessionDurationMilliseconds: dur ?? 0,
|
||||
sessionPauseMilliseconds: pause ?? 0
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -78,4 +119,8 @@ export interface SerializedTimeLimitRule {
|
||||
time: number
|
||||
days: number
|
||||
extraTime: boolean
|
||||
start?: number
|
||||
end?: number
|
||||
dur?: number
|
||||
pause?: number
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -20,6 +20,7 @@ export interface ClientDataStatus {
|
||||
apps: {[key: string]: string} // installedAppsVersionsByDeviceId
|
||||
categories: {[key: string]: CategoryDataStatus}
|
||||
users: string // userListVersion
|
||||
clientLevel?: number
|
||||
}
|
||||
|
||||
export interface CategoryDataStatus {
|
||||
|
||||
@@ -121,12 +121,44 @@ export interface ServerUpdatedCategoryAssignedApps {
|
||||
export interface ServerUpdatedCategoryUsedTimes {
|
||||
categoryId: string
|
||||
times: Array<ServerUsedTimeItem>
|
||||
sessionDurations: Array<ServerSessionDurationItem>
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface ServerUsedTimeItem {
|
||||
day: number // day of epoch
|
||||
time: number // in milliseconds
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface ServerSessionDurationItem {
|
||||
/**
|
||||
* the maximum duration of a session (maxSessionDuration)
|
||||
*/
|
||||
md: number
|
||||
/**
|
||||
* the pause duration after a session (sessionPauseDuration)
|
||||
*/
|
||||
spd: number
|
||||
/**
|
||||
* the start minute of the day of the session/ the rule
|
||||
* which created this session (startMinuteOfDay)
|
||||
*/
|
||||
sm: number
|
||||
/**
|
||||
* the end minute of the day of the session/ the rule
|
||||
* which created this session (endMinuteOfDay)
|
||||
*/
|
||||
em: number
|
||||
/**
|
||||
* the timestamp of the last usage of this session (lastUsage)
|
||||
*/
|
||||
l: number
|
||||
/**
|
||||
* the duration of the last/ current session (lastSessionDuration)
|
||||
*/
|
||||
d: number
|
||||
}
|
||||
|
||||
export interface ServerUpdatedTimeLimitRules {
|
||||
@@ -140,6 +172,10 @@ export interface ServerTimeLimitRule {
|
||||
extraTime: boolean // applyToExtraTimeUsage
|
||||
dayMask: number // as binary bitmask
|
||||
maxTime: number // maximumTimeInMillis
|
||||
start: number // startMinuteOfDay
|
||||
end: number // endMinuteOfDay
|
||||
session: number // maximum session duration
|
||||
pause: number // session pause duration
|
||||
}
|
||||
|
||||
export interface ServerInstalledAppsData {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const MinuteOfDay = {
|
||||
MIN: 0,
|
||||
MAX: 24 * 60 - 1,
|
||||
LENGTH: 24 * 60
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 Jonas Lochmann
|
||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
@@ -45,7 +45,7 @@ async function deleteOldUsedTimes ({ database }: {
|
||||
|
||||
await database.transaction(async (transaction) => {
|
||||
// get matching categories
|
||||
const categoriesToCleanUp = await database.usedTime.findAll({
|
||||
const categoriesToCleanUpOne = await database.usedTime.findAll({
|
||||
transaction,
|
||||
where: {
|
||||
lastUpdate: {
|
||||
@@ -56,12 +56,33 @@ async function deleteOldUsedTimes ({ database }: {
|
||||
'familyId',
|
||||
'categoryId'
|
||||
],
|
||||
limit: 100
|
||||
limit: 1000,
|
||||
order: [['lastUpdate', 'ASC']]
|
||||
}).map((item) => ({
|
||||
familyId: item.familyId,
|
||||
categoryId: item.categoryId
|
||||
}))
|
||||
|
||||
const categoriesToCleanUpTwo = await database.sessionDuration.findAll({
|
||||
transaction,
|
||||
where: {
|
||||
roundedLastUpdate: {
|
||||
[Sequelize.Op.lt]: (now - 1000 * 60 * 60 * 24 * 3 /* 3 days */).toString()
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
'familyId',
|
||||
'categoryId'
|
||||
],
|
||||
limit: 1000,
|
||||
order: [['roundedLastUpdate', 'ASC']]
|
||||
}).map((item) => ({
|
||||
familyId: item.familyId,
|
||||
categoryId: item.categoryId
|
||||
}))
|
||||
|
||||
const categoriesToCleanUp = [ ...categoriesToCleanUpOne, ...categoriesToCleanUpTwo ]
|
||||
|
||||
const distinctCategoriesToCleanUp = uniqBy(categoriesToCleanUp, (item) => item.familyId + '_' + item.categoryId)
|
||||
|
||||
if (distinctCategoriesToCleanUp.length > 0) {
|
||||
@@ -78,6 +99,18 @@ async function deleteOldUsedTimes ({ database }: {
|
||||
}
|
||||
})
|
||||
|
||||
await database.sessionDuration.destroy({
|
||||
transaction,
|
||||
where: {
|
||||
[Sequelize.Op.or]: (
|
||||
distinctCategoriesToCleanUp
|
||||
),
|
||||
roundedLastUpdate: {
|
||||
[Sequelize.Op.lt]: (now - 1000 * 60 * 60 * 24 * 3 /* 3 days */).toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// invalidiate categories
|
||||
await database.category.update({
|
||||
usedTimesVersion: generateVersionId()
|
||||
|
||||
Reference in New Issue
Block a user