1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createHash } from 'crypto';
import { Inject, Injectable } from '@nestjs/common';
import { LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { SkApFetchLog, SkApInboxLog, SkApContext } from '@/models/_.js';
import type { ApContextsRepository, ApFetchLogsRepository, ApInboxLogsRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import { JsonValue } from '@/misc/json-value.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdService } from '@/core/IdService.js';
import { IActivity, IObject } from './activitypub/type.js';
@Injectable()
export class ApLogService {
constructor(
@Inject(DI.config)
private readonly config: Config,
@Inject(DI.apContextsRepository)
private apContextsRepository: ApContextsRepository,
@Inject(DI.apInboxLogsRepository)
private readonly apInboxLogsRepository: ApInboxLogsRepository,
@Inject(DI.apFetchLogsRepository)
private readonly apFetchLogsRepository: ApFetchLogsRepository,
private readonly utilityService: UtilityService,
private readonly idService: IdService,
) {}
/**
* Creates an inbox log from an activity, and saves it if pre-save is enabled.
*/
public async createInboxLog(data: Partial<SkApInboxLog> & {
activity: IActivity,
keyId: string,
}): Promise<SkApInboxLog> {
const { object: activity, context, contextHash } = extractObjectContext(data.activity);
const host = this.utilityService.extractDbHost(data.keyId);
const log = new SkApInboxLog({
id: this.idService.gen(),
at: new Date(),
verified: false,
accepted: false,
host,
...data,
activity,
context,
contextHash,
});
if (this.config.activityLogging.preSave) {
await this.saveInboxLog(log);
}
return log;
}
/**
* Saves or finalizes an inbox log.
*/
public async saveInboxLog(log: SkApInboxLog): Promise<SkApInboxLog> {
if (log.context) {
await this.saveContext(log.context);
}
// Will be UPDATE with preSave, and INSERT without.
await this.apInboxLogsRepository.upsert(log, ['id']);
return log;
}
/**
* Creates a fetch log from an activity, and saves it if pre-save is enabled.
*/
public async createFetchLog(data: Partial<SkApFetchLog> & {
requestUri: string
host: string,
}): Promise<SkApFetchLog> {
const log = new SkApFetchLog({
id: this.idService.gen(),
at: new Date(),
accepted: false,
...data,
});
if (this.config.activityLogging.preSave) {
await this.saveFetchLog(log);
}
return log;
}
/**
* Saves or finalizes a fetch log.
*/
public async saveFetchLog(log: SkApFetchLog): Promise<SkApFetchLog> {
if (log.context) {
await this.saveContext(log.context);
}
// Will be UPDATE with preSave, and INSERT without.
await this.apFetchLogsRepository.upsert(log, ['id']);
return log;
}
private async saveContext(context: SkApContext): Promise<void> {
// https://stackoverflow.com/a/47064558
await this.apContextsRepository
.createQueryBuilder('activity_context')
.insert()
.into(SkApContext)
.values(context)
.orIgnore('md5')
.execute();
}
/**
* Deletes all expired AP logs and garbage-collects the AP context cache.
* Returns the total number of deleted rows.
*/
public async deleteExpiredLogs(): Promise<number> {
// This is the date in UTC of the oldest log to KEEP
const oldestAllowed = new Date(Date.now() - this.config.activityLogging.maxAge);
// Delete all logs older than the threshold.
const inboxDeleted = await this.deleteExpiredInboxLogs(oldestAllowed);
const fetchDeleted = await this.deleteExpiredFetchLogs(oldestAllowed);
return inboxDeleted + fetchDeleted;
}
private async deleteExpiredInboxLogs(oldestAllowed: Date): Promise<number> {
const { affected } = await this.apInboxLogsRepository.delete({
at: LessThan(oldestAllowed),
});
return affected ?? 0;
}
private async deleteExpiredFetchLogs(oldestAllowed: Date): Promise<number> {
const { affected } = await this.apFetchLogsRepository.delete({
at: LessThan(oldestAllowed),
});
return affected ?? 0;
}
}
export function extractObjectContext<T extends IObject>(input: T) {
const object = Object.assign({}, input, { '@context': undefined }) as Omit<T, '@context'>;
const { context, contextHash } = parseContext(input['@context']);
return { object, context, contextHash };
}
export function parseContext(input: JsonValue | undefined): { contextHash: string | null, context: SkApContext | null } {
// Empty contexts are excluded for easier querying
if (input == null) {
return {
contextHash: null,
context: null,
};
}
const contextHash = createHash('md5').update(JSON.stringify(input)).digest('base64');
const context = new SkApContext({
md5: contextHash,
json: input,
});
return { contextHash, context };
}
export function calculateDurationSince(startTime: bigint): number {
// Calculate the processing time with correct rounding and decimals.
// 1. Truncate nanoseconds to microseconds
// 2. Scale to 1/10 millisecond ticks.
// 3. Round to nearest tick.
// 4. Sale to milliseconds
// Example: 123,456,789 ns -> 123,456 us -> 12,345.6 ticks -> 12,346 ticks -> 123.46 ms
const endTime = process.hrtime.bigint();
return Math.round(Number((endTime - startTime) / 1000n) / 10) / 100;
}
|