Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1,46 @@
import { publishMainStream } from "../../../../../services/stream.js";
import * as OTPAuth from "otpauth";
import define from "../../../define.js";
import { Users, UserProfiles } from "../../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
token: {
type: "string"
}
},
required: [
"token"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const token = ps.token.replace(/\s/g, "");
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
if (profile.twoFactorTempSecret == null) {
throw new Error("二段階認証の設定が開始されていません");
}
const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
digits: 6,
token,
window: 1
});
if (delta === null) {
throw new Error("not verified");
}
await UserProfiles.update(user.id, {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
publishMainStream(user.id, "meUpdated", iObj);
});
@@ -0,0 +1,125 @@
import * as cbor from "cbor";
import define from "../../../define.js";
import { UserProfiles, UserSecurityKeys, AttestationChallenges, Users } from "../../../../../models/index.js";
import config from "../../../../../config/index.js";
import { procedures, hash } from "../../../2fa.js";
import { publishMainStream } from "../../../../../services/stream.js";
import { comparePassword } from "../../../../../misc/password.js";
const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8"));
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
clientDataJSON: {
type: "string"
},
attestationObject: {
type: "string"
},
password: {
type: "string"
},
challengeId: {
type: "string"
},
name: {
type: "string",
minLength: 1,
maxLength: 30
}
},
required: [
"clientDataJSON",
"attestationObject",
"password",
"challengeId",
"name"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
if (!profile.twoFactorEnabled) {
throw new Error("2fa not enabled");
}
const clientData = JSON.parse(ps.clientDataJSON);
if (clientData.type !== "webauthn.create") {
throw new Error("not a creation attestation");
}
if (clientData.origin !== `${config.scheme}://${config.host}`) {
throw new Error("origin mismatch");
}
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, "utf-8"));
const attestation = await cbor.decodeFirst(ps.attestationObject);
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error("rpIdHash mismatch");
}
const flags = attestation.authData[32];
if (!(flags & 1)) {
throw new Error("user not present");
}
const authData = Buffer.from(attestation.authData);
const credentialIdLength = authData.readUInt16BE(53);
const credentialId = authData.slice(55, 55 + credentialIdLength);
const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey = await cbor.decodeFirst(publicKeyData);
if (publicKey.get(3) !== -7) {
throw new Error("alg mismatch");
}
if (!procedures[attestation.fmt]) {
throw new Error("unsupported fmt");
}
const verificationData = procedures[attestation.fmt].verify({
attStmt: attestation.attStmt,
authenticatorData: authData,
clientDataHash: clientDataJSONHash,
credentialId,
publicKey,
rpIdHash
});
if (!verificationData.valid) throw new Error("signature invalid");
const attestationChallenge = await AttestationChallenges.findOneBy({
userId: user.id,
id: ps.challengeId,
registrationChallenge: true,
challenge: hash(clientData.challenge).toString("hex")
});
if (!attestationChallenge) {
throw new Error("non-existent challenge");
}
await AttestationChallenges.delete({
userId: user.id,
id: ps.challengeId
});
// Expired challenge (> 5min old)
if (new Date().getTime() - attestationChallenge.createdAt.getTime() >= 5 * 60 * 1000) {
throw new Error("expired challenge");
}
const credentialIdString = credentialId.toString("hex");
await UserSecurityKeys.insert({
userId: user.id,
id: credentialIdString,
lastUsed: new Date(),
name: ps.name,
publicKey: verificationData.publicKey.toString("hex")
});
// Publish meUpdated event
publishMainStream(user.id, "meUpdated", await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
}));
return {
id: credentialIdString,
name: ps.name
};
});
@@ -0,0 +1,55 @@
import define from "../../../define.js";
import { Users, UserProfiles, UserSecurityKeys } from "../../../../../models/index.js";
import { publishMainStream } from "../../../../../services/stream.js";
import { ApiError } from "../../../error.js";
export const meta = {
requireCredential: true,
secure: true,
errors: {
noKey: {
message: "No security key.",
code: "NO_SECURITY_KEY",
id: "f9c54d7f-d4c2-4d3c-9a8g-a70daac86512"
}
}
};
export const paramDef = {
type: "object",
properties: {
value: {
type: "boolean"
}
},
required: [
"value"
]
};
export default define(meta, paramDef, async (ps, user)=>{
if (ps.value === true) {
// セキュリティキーがなければパスワードレスを有効にはできない
const keyCount = await UserSecurityKeys.count({
where: {
userId: user.id
},
select: {
id: true,
name: true,
lastUsed: true
}
});
if (keyCount === 0) {
await UserProfiles.update(user.id, {
usePasswordLessLogin: false
});
throw new ApiError(meta.errors.noKey);
}
}
await UserProfiles.update(user.id, {
usePasswordLessLogin: ps.value
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
publishMainStream(user.id, "meUpdated", iObj);
});
@@ -0,0 +1,51 @@
import define from "../../../define.js";
import { UserProfiles, AttestationChallenges } from "../../../../../models/index.js";
import { promisify } from "node:util";
import * as crypto from "node:crypto";
import { genId } from "../../../../../misc/gen-id.js";
import { hash } from "../../../2fa.js";
import { comparePassword } from "../../../../../misc/password.js";
const randomBytes = promisify(crypto.randomBytes);
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
// if (!profile.twoFactorEnabled) {
// throw new Error("2fa not enabled");
// }
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
const challengeId = genId();
await AttestationChallenges.insert({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, "utf-8")).toString("hex"),
createdAt: new Date(),
registrationChallenge: true
});
return {
challengeId,
challenge
};
});
@@ -0,0 +1,52 @@
import * as OTPAuth from "otpauth";
import * as QRCode from "qrcode";
import config from "../../../../../config/index.js";
import { UserProfiles } from "../../../../../models/index.js";
import define from "../../../define.js";
import { comparePassword } from "../../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
// Generate user's secret key
const secret = new OTPAuth.Secret();
await UserProfiles.update(user.id, {
twoFactorTempSecret: secret.base32
});
// Get the data URL of the authenticator URL
const totp = new OTPAuth.TOTP({
secret,
digits: 6,
label: user.username,
issuer: config.host
});
const url = totp.toString();
const qr = await QRCode.toDataURL(url);
return {
qr,
url,
secret: secret.base32,
label: user.username,
issuer: config.host
};
});
@@ -0,0 +1,60 @@
import { comparePassword } from "../../../../../misc/password.js";
import define from "../../../define.js";
import { UserProfiles, UserSecurityKeys, Users } from "../../../../../models/index.js";
import { publishMainStream } from "../../../../../services/stream.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
},
credentialId: {
type: "string"
}
},
required: [
"password",
"credentialId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
// Make sure we only delete the user's own creds
await UserSecurityKeys.delete({
userId: user.id,
id: ps.credentialId
});
// 使われているキーがなくなったらパスワードレスログインをやめる
const keyCount = await UserSecurityKeys.count({
where: {
userId: user.id
},
select: {
id: true,
name: true,
lastUsed: true
}
});
if (keyCount === 0) {
await UserProfiles.update(me.id, {
usePasswordLessLogin: false
});
}
// Publish meUpdated event
publishMainStream(user.id, "meUpdated", await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
}));
return {};
});
@@ -0,0 +1,39 @@
import { publishMainStream } from "../../../../../services/stream.js";
import define from "../../../define.js";
import { Users, UserProfiles } from "../../../../../models/index.js";
import { comparePassword } from "../../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
await UserProfiles.update(user.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
usePasswordLessLogin: false
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
publishMainStream(user.id, "meUpdated", iObj);
});
@@ -0,0 +1,56 @@
import { publishMainStream } from "../../../../../services/stream.js";
import define from "../../../define.js";
import { Users, UserSecurityKeys } from "../../../../../models/index.js";
import { ApiError } from "../../../error.js";
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: "No such key.",
code: "NO_SUCH_KEY",
id: "f9c5467f-d492-4d3c-9a8g-a70dacc86512"
},
accessDenied: {
message: "You do not have edit privilege of the channel.",
code: "ACCESS_DENIED",
id: "1fb7cb09-d46a-4fff-b8df-057708cce513"
}
}
};
export const paramDef = {
type: "object",
properties: {
name: {
type: "string",
minLength: 1,
maxLength: 30
},
credentialId: {
type: "string"
}
},
required: [
"name",
"credentialId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const key = await UserSecurityKeys.findOneBy({
id: ps.credentialId
});
if (key == null) {
throw new ApiError(meta.errors.noSuchKey);
}
if (key.userId !== user.id) {
throw new ApiError(meta.errors.accessDenied);
}
await UserSecurityKeys.update(key.id, {
name: ps.name
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
publishMainStream(user.id, "meUpdated", iObj);
});
@@ -0,0 +1,51 @@
import define from "../../define.js";
import { AccessTokens } from "../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
sort: {
type: "string",
enum: [
"+createdAt",
"-createdAt",
"+lastUsedAt",
"-lastUsedAt"
]
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = AccessTokens.createQueryBuilder("token").where("token.userId = :userId", {
userId: user.id
});
switch(ps.sort){
case "+createdAt":
query.orderBy("token.createdAt", "DESC");
break;
case "-createdAt":
query.orderBy("token.createdAt", "ASC");
break;
case "+lastUsedAt":
query.orderBy("token.lastUsedAt", "DESC");
break;
case "-lastUsedAt":
query.orderBy("token.lastUsedAt", "ASC");
break;
default:
query.orderBy("token.id", "ASC");
break;
}
const tokens = await query.getMany();
return await Promise.all(tokens.map((token)=>({
id: token.id,
name: token.name,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
permission: token.permission
})));
});
@@ -0,0 +1,46 @@
import define from "../../define.js";
import { AccessTokens, Apps } from "../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
},
sort: {
type: "string",
enum: [
"desc",
"asc"
],
default: "desc"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
// Get tokens
const tokens = await AccessTokens.find({
where: {
userId: user.id
},
take: ps.limit,
skip: ps.offset,
order: {
id: ps.sort === "asc" ? 1 : -1
}
});
return await Promise.all(tokens.map((token)=>Apps.pack(token.appId, user, {
detail: true
})));
});
@@ -0,0 +1,38 @@
import define from "../../define.js";
import { UserProfiles } from "../../../../models/index.js";
import { hashPassword, comparePassword } from "../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
currentPassword: {
type: "string"
},
newPassword: {
type: "string",
minLength: 1
}
},
required: [
"currentPassword",
"newPassword"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.currentPassword, profile.password);
if (!same) {
throw new Error("incorrect password");
}
// Generate hash of password
const hash = await hashPassword(ps.newPassword);
await UserProfiles.update(user.id, {
password: hash
});
});
@@ -0,0 +1,36 @@
import { UserProfiles, Users } from "../../../../models/index.js";
import { deleteAccount } from "../../../../services/delete-account.js";
import define from "../../define.js";
import { comparePassword } from "../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
const userDetailed = await Users.findOneByOrFail({
id: user.id
});
if (userDetailed.isDeleted) {
return;
}
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
await deleteAccount(user);
});
@@ -0,0 +1,19 @@
import define from "../../define.js";
import { createExportBlockingJob } from "../../../../queue/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
}
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
createExportBlockingJob(user);
});
@@ -0,0 +1,28 @@
import define from "../../define.js";
import { createExportFollowingJob } from "../../../../queue/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
}
};
export const paramDef = {
type: "object",
properties: {
excludeMuting: {
type: "boolean",
default: false
},
excludeInactive: {
type: "boolean",
default: false
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
createExportFollowingJob(user, ps.excludeMuting, ps.excludeInactive);
});
@@ -0,0 +1,19 @@
import define from "../../define.js";
import { createExportMuteJob } from "../../../../queue/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
}
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
createExportMuteJob(user);
});
@@ -0,0 +1,19 @@
import define from "../../define.js";
import { createExportNotesJob } from "../../../../queue/index.js";
import { DAY } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: DAY,
max: 1
}
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
createExportNotesJob(user);
});
@@ -0,0 +1,19 @@
import define from "../../define.js";
import { createExportUserListsJob } from "../../../../queue/index.js";
import { MINUTE } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: MINUTE,
max: 1
}
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
createExportUserListsJob(user);
});
@@ -0,0 +1,50 @@
import define from "../../define.js";
import { NoteFavorites } from "../../../../models/index.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"notes",
"favorites"
],
requireCredential: true,
kind: "read:favorites",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "NoteFavorite"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(NoteFavorites.createQueryBuilder("favorite"), ps.sinceId, ps.untilId).andWhere("favorite.userId = :meId", {
meId: user.id
}).leftJoinAndSelect("favorite.note", "note");
const favorites = await query.take(ps.limit).getMany();
return await NoteFavorites.packMany(favorites, user);
});
@@ -0,0 +1,62 @@
import define from "../../../define.js";
import { GalleryLikes } from "../../../../../models/index.js";
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"gallery"
],
requireCredential: true,
kind: "read:gallery-likes",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id"
},
post: {
type: "object",
optional: false,
nullable: false,
ref: "GalleryPost"
}
}
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(GalleryLikes.createQueryBuilder("like"), ps.sinceId, ps.untilId).andWhere("like.userId = :meId", {
meId: user.id
}).leftJoinAndSelect("like.post", "post");
const likes = await query.take(ps.limit).getMany();
return await GalleryLikes.packMany(likes, user);
});
@@ -0,0 +1,49 @@
import { GalleryPosts } from "../../../../../models/index.js";
import define from "../../../define.js";
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"gallery"
],
requireCredential: true,
kind: "read:gallery",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "GalleryPost"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(GalleryPosts.createQueryBuilder("post"), ps.sinceId, ps.untilId).andWhere("post.userId = :meId", {
meId: user.id
});
const posts = await query.take(ps.limit).getMany();
return await GalleryPosts.packMany(posts, user);
});
@@ -0,0 +1,57 @@
import define from "../../define.js";
import { createImportBlockingJob } from "../../../../queue/index.js";
import { ApiError } from "../../error.js";
import { DriveFiles } from "../../../../models/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "ebb53e5f-6574-9c0c-0b92-7ca6def56d7e"
},
unexpectedFileType: {
message: "We need csv file.",
code: "UNEXPECTED_FILE_TYPE",
id: "b6fab7d6-d945-d67c-dfdb-32da1cd12cfe"
},
tooBigFile: {
message: "That file is too big.",
code: "TOO_BIG_FILE",
id: "b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf"
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "6f3a4dcc-f060-a707-4950-806fbdbe60d6"
}
}
};
export const paramDef = {
type: "object",
properties: {
fileId: {
type: "string",
format: "misskey:id"
}
},
required: [
"fileId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const file = await DriveFiles.findOneBy({
id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
createImportBlockingJob(user, file.id);
});
@@ -0,0 +1,57 @@
import define from "../../define.js";
import { createImportFollowingJob } from "../../../../queue/index.js";
import { ApiError } from "../../error.js";
import { DriveFiles } from "../../../../models/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duratition: HOUR,
max: 1
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "b98644cf-a5ac-4277-a502-0b8054a709a3"
},
unexpectedFileType: {
message: "Must be a CSV or JSON file.",
code: "UNEXPECTED_FILE_TYPE",
id: "660f3599-bce0-4f95-9dde-311fd841c183"
},
tooBigFile: {
message: "That file is too big.",
code: "TOO_BIG_FILE",
id: "dee9d4ed-ad07-43ed-8b34-b2856398bc60"
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "31a1b42c-06f7-42ae-8a38-a661c5c9f691"
}
}
};
export const paramDef = {
type: "object",
properties: {
fileId: {
type: "string",
format: "misskey:id"
}
},
required: [
"fileId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const file = await DriveFiles.findOneBy({
id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.size > 2_000_000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
createImportFollowingJob(user, file.id);
});
@@ -0,0 +1,57 @@
import define from "../../define.js";
import { createImportMutingJob } from "../../../../queue/index.js";
import { ApiError } from "../../error.js";
import { DriveFiles } from "../../../../models/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "e674141e-bd2a-ba85-e616-aefb187c9c2a"
},
unexpectedFileType: {
message: "We need csv file.",
code: "UNEXPECTED_FILE_TYPE",
id: "568c6e42-c86c-ba09-c004-517f83f9f1a8"
},
tooBigFile: {
message: "That file is too big.",
code: "TOO_BIG_FILE",
id: "9b4ada6d-d7f7-0472-0713-4f558bd1ec9c"
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "d2f12af1-e7b4-feac-86a3-519548f2728e"
}
}
};
export const paramDef = {
type: "object",
properties: {
fileId: {
type: "string",
format: "misskey:id"
}
},
required: [
"fileId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const file = await DriveFiles.findOneBy({
id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
createImportMutingJob(user, file.id);
});
@@ -0,0 +1,46 @@
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { DAY } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: DAY * 30,
max: 2
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "e674141e-bd2a-ba85-e616-aefb187c9c2a"
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "d2f12af1-e7b4-feac-86a3-519548f2728e"
},
importsDisabled: {
message: "Post imports are disabled for security reasons.",
code: "IMPORTS_DISABLED",
id: " bc9227e4-fb82-11ed-be56-0242ac120002"
}
}
};
export const paramDef = {
type: "object",
properties: {
fileId: {
type: "string",
format: "misskey:id"
},
signatureCheck: {
type: "boolean"
}
},
required: [
"fileId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
throw new ApiError(meta.errors.importsDisabled);
});
@@ -0,0 +1,57 @@
import define from "../../define.js";
import { createImportUserListsJob } from "../../../../queue/index.js";
import { ApiError } from "../../error.js";
import { DriveFiles } from "../../../../models/index.js";
import { HOUR } from "../../../../const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "ea9cc34f-c415-4bc6-a6fe-28ac40357049"
},
unexpectedFileType: {
message: "We need csv file.",
code: "UNEXPECTED_FILE_TYPE",
id: "a3c9edda-dd9b-4596-be6a-150ef813745c"
},
tooBigFile: {
message: "That file is too big.",
code: "TOO_BIG_FILE",
id: "ae6e7a22-971b-4b52-b2be-fc0b9b121fe9"
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "99efe367-ce6e-4d44-93f8-5fae7b040356"
}
}
};
export const paramDef = {
type: "object",
properties: {
fileId: {
type: "string",
format: "misskey:id"
}
},
required: [
"fileId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const file = await DriveFiles.findOneBy({
id: ps.fileId
});
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
createImportUserListsJob(user, file.id);
});
@@ -0,0 +1,95 @@
import { Users } from "../../../../models/index.js";
import { resolveUser } from "../../../../remote/resolve-user.js";
import acceptAllFollowRequests from "../../../../services/following/requests/accept-all.js";
import { publishToFollowers } from "../../../../services/i/update.js";
import { publishMainStream } from "../../../../services/stream.js";
import { DAY } from "../../../../const.js";
import { apiLogger } from "../../logger.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { parse } from "../../../../misc/acct.js";
export const meta = {
tags: [
"users"
],
secure: true,
requireCredential: true,
limit: {
duration: DAY,
max: 30
},
errors: {
noSuchUser: {
message: "No such user.",
code: "NO_SUCH_USER",
id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5"
},
notRemote: {
message: "User is not remote. You can only migrate to other instances.",
code: "NOT_REMOTE",
id: "4362f8dc-731f-4ad8-a694-be2a88922a24"
},
uriNull: {
message: "User ActivityPup URI is null.",
code: "URI_NULL",
id: "bf326f31-d430-4f97-9933-5d61e4d48a23"
},
alreadyMoved: {
message: "You have already moved your account.",
code: "ALREADY_MOVED",
id: "56f20ec9-fd06-4fa5-841b-edd6d7d4fa31"
},
yourself: {
message: "You can't set yourself as your own alias.",
code: "FORBIDDEN_TO_SET_YOURSELF",
id: "25c90186-4ab0-49c8-9bba-a1fa6c202ba4"
}
}
};
export const paramDef = {
type: "object",
properties: {
alsoKnownAs: {
type: "array",
maxItems: 10,
uniqueItems: true,
items: {
type: "string"
}
}
},
required: [
"alsoKnownAs"
]
};
export default define(meta, paramDef, async (ps, user)=>{
if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
if (user.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
const newAka = new Set();
for (const line of ps.alsoKnownAs){
if (!line) throw new ApiError(meta.errors.noSuchUser);
const { username, host } = parse(line);
const aka = await resolveUser(username, host).catch((e)=>{
apiLogger.warn(`failed to resolve remote user: ${e}`);
throw new ApiError(meta.errors.noSuchUser);
});
if (aka.id === user.id) throw new ApiError(meta.errors.yourself);
if (!aka.uri) throw new ApiError(meta.errors.uriNull);
newAka.add(aka.uri);
}
const updates = {
alsoKnownAs: newAka.size > 0 ? Array.from(newAka) : null
};
await Users.update(user.id, updates);
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
// Publish meUpdated event
publishMainStream(user.id, "meUpdated", iObj);
if (user.isLocked === false) {
acceptAllFollowRequests(user);
}
publishToFollowers(user.id);
return iObj;
});
@@ -0,0 +1,147 @@
import { resolveUser } from "../../../../remote/resolve-user.js";
import { DAY } from "../../../../const.js";
import DeliverManager from "../../../../remote/activitypub/deliver-manager.js";
import { renderActivity } from "../../../../remote/activitypub/renderer/index.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { apiLogger } from "../../logger.js";
import deleteFollowing from "../../../../services/following/delete.js";
import create from "../../../../services/following/create.js";
import { getUser } from "../../common/getters.js";
import { Followings, Users } from "../../../../models/index.js";
import config from "../../../../config/index.js";
import { publishMainStream } from "../../../../services/stream.js";
import { parse } from "../../../../misc/acct.js";
export const meta = {
tags: [
"users"
],
secure: true,
requireCredential: true,
limit: {
duration: DAY,
max: 5
},
errors: {
noSuchMoveTarget: {
message: "No such move target.",
code: "NO_SUCH_MOVE_TARGET",
id: "b5c90186-4ab0-49c8-9bba-a1f76c202ba4"
},
remoteAccountForbids: {
message: "Remote account doesn't have proper 'Known As' alias. Did you remember to set it?",
code: "REMOTE_ACCOUNT_FORBIDS",
id: "b5c90186-4ab0-49c8-9bba-a1f766282ba4"
},
notRemote: {
message: "User is not remote. You can only migrate to other instances.",
code: "NOT_REMOTE",
id: "4362f8dc-731f-4ad8-a694-be2a88922a24"
},
adminForbidden: {
message: "Admins cant migrate.",
code: "NOT_ADMIN_FORBIDDEN",
id: "4362e8dc-731f-4ad8-a694-be2a88922a24"
},
noSuchUser: {
message: "No such user.",
code: "NO_SUCH_USER",
id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5"
},
uriNull: {
message: "User ActivityPup URI is null.",
code: "URI_NULL",
id: "bf326f31-d430-4f97-9933-5d61e4d48a23"
},
localUriNull: {
message: "Local User ActivityPup URI is null.",
code: "URI_NULL",
id: "95ba11b9-90e8-43a5-ba16-7acc1ab32e71"
},
alreadyMoved: {
message: "Account was already moved to another account.",
code: "ALREADY_MOVED",
id: "b234a14e-9ebe-4581-8000-074b3c215962"
}
}
};
export const paramDef = {
type: "object",
properties: {
moveToAccount: {
type: "string"
}
},
required: [
"moveToAccount"
]
};
function moveActivity(toUrl, fromUrl) {
const activity = {
id: null,
actor: fromUrl,
type: "Move",
object: fromUrl,
target: toUrl
};
return renderActivity(activity);
}
export default define(meta, paramDef, async (ps, user)=>{
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
if (user.isAdmin) throw new ApiError(meta.errors.adminForbidden);
if (user.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
const { username, host } = parse(ps.moveToAccount);
if (!host) throw new ApiError(meta.errors.notRemote);
const moveTo = await resolveUser(username, host).catch((e)=>{
apiLogger.warn(`failed to resolve remote user: ${e}`);
throw new ApiError(meta.errors.noSuchMoveTarget);
});
let fromUrl = user.uri;
if (!fromUrl) {
fromUrl = `${config.url}/users/${user.id}`;
}
let toUrl = moveTo.uri;
if (!toUrl) {
throw new ApiError(meta.errors.uriNull);
}
let allowed = false;
moveTo.alsoKnownAs?.forEach((element)=>{
if (fromUrl.includes(element)) allowed = true;
});
if (!(allowed && toUrl && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids);
const updates = {};
if (!toUrl) toUrl = "";
updates.movedToUri = toUrl;
updates.alsoKnownAs = user.alsoKnownAs?.concat(toUrl) ?? [
toUrl
];
await Users.update(user.id, updates);
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
const moveAct = moveActivity(toUrl, fromUrl);
const dm = new DeliverManager(user, moveAct);
dm.addFollowersRecipe();
dm.execute();
// Publish meUpdated event
publishMainStream(user.id, "meUpdated", iObj);
const followings = await Followings.findBy({
followeeId: user.id
});
followings.forEach(async (following)=>{
//if follower is local
if (!following.followerHost) {
const follower = await getUser(following.followerId).catch((e)=>{
if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") throw new ApiError(meta.errors.noSuchUser);
throw e;
});
await deleteFollowing(follower, user);
try {
await create(follower, moveTo);
} catch (e) {
/* empty */ }
}
});
return iObj;
});
@@ -0,0 +1,145 @@
import { Brackets } from "typeorm";
import { Notifications, Followings, Mutings, Users, UserProfiles } from "../../../../models/index.js";
import { notificationTypes } from "../../../../types.js";
import read from "../../../../services/note/read.js";
import { readNotification } from "../../common/read-notification.js";
import define from "../../define.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"notifications"
],
requireCredential: true,
limit: {
duration: 60000,
max: 15
},
kind: "read:notifications",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Notification"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
following: {
type: "boolean",
default: false
},
unreadOnly: {
type: "boolean",
default: false
},
markAsRead: {
type: "boolean",
default: true
},
includeTypes: {
type: "array",
items: {
type: "string",
enum: notificationTypes
}
},
excludeTypes: {
type: "array",
items: {
type: "string",
enum: notificationTypes
}
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
// includeTypes が空の場合はクエリしない
if (ps.includeTypes && ps.includeTypes.length === 0) {
return [];
}
// excludeTypes に全指定されている場合はクエリしない
if (notificationTypes.every((type)=>ps.excludeTypes?.includes(type))) {
return [];
}
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :followerId", {
followerId: user.id
});
const mutingQuery = Mutings.createQueryBuilder("muting").select("muting.muteeId").where("muting.muterId = :muterId", {
muterId: user.id
});
const mutingInstanceQuery = UserProfiles.createQueryBuilder("user_profile").select("user_profile.mutedInstances").where("user_profile.userId = :muterId", {
muterId: user.id
});
const suspendedQuery = Users.createQueryBuilder("users").select("users.id").where("users.isSuspended = TRUE");
const query = makePaginationQuery(Notifications.createQueryBuilder("notification"), ps.sinceId, ps.untilId).andWhere("notification.notifieeId = :meId", {
meId: user.id
}).leftJoinAndSelect("notification.notifier", "notifier").leftJoinAndSelect("notification.note", "note").leftJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
// muted users
query.andWhere(new Brackets((qb)=>{
qb.where(`notification.notifierId NOT IN (${mutingQuery.getQuery()})`).orWhere("notification.notifierId IS NULL");
}));
query.setParameters(mutingQuery.getParameters());
// muted instances
query.andWhere(new Brackets((qb)=>{
qb.andWhere("notifier.host IS NULL").orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
query.setParameters(mutingInstanceQuery.getParameters());
// suspended users
query.andWhere(new Brackets((qb)=>{
qb.where(`notification.notifierId NOT IN (${suspendedQuery.getQuery()})`).orWhere("notification.notifierId IS NULL");
}));
if (ps.following) {
query.andWhere(`((notification.notifierId IN (${followingQuery.getQuery()})) OR (notification.notifierId = :meId))`, {
meId: user.id
});
query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes && ps.includeTypes.length > 0) {
query.andWhere("notification.type IN (:...includeTypes)", {
includeTypes: ps.includeTypes
});
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
query.andWhere("notification.type NOT IN (:...excludeTypes)", {
excludeTypes: ps.excludeTypes
});
}
if (ps.unreadOnly) {
query.andWhere("notification.isRead = false");
}
const notifications = await query.take(ps.limit).getMany();
// Mark all as read
if (notifications.length > 0 && ps.markAsRead) {
readNotification(user.id, notifications.map((x)=>x.id));
}
const notes = notifications.filter((notification)=>[
"mention",
"reply",
"quote"
].includes(notification.type)).map((notification)=>notification.note);
if (notes.length > 0) {
read(user.id, notes);
}
return await Notifications.packMany(notifications, user.id);
});
@@ -0,0 +1,60 @@
import { PageLikes } from "../../../../models/index.js";
import define from "../../define.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"pages"
],
requireCredential: true,
kind: "read:page-likes",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id"
},
page: {
type: "object",
optional: false,
nullable: false,
ref: "Page"
}
}
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(PageLikes.createQueryBuilder("like"), ps.sinceId, ps.untilId).andWhere("like.userId = :meId", {
meId: user.id
}).leftJoinAndSelect("like.page", "page");
const likes = await query.take(ps.limit).getMany();
return PageLikes.packMany(likes, user);
});
@@ -0,0 +1,49 @@
import { Pages } from "../../../../models/index.js";
import define from "../../define.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"pages"
],
requireCredential: true,
kind: "read:pages",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Page"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(Pages.createQueryBuilder("page"), ps.sinceId, ps.untilId).andWhere("page.userId = :meId", {
meId: user.id
});
const pages = await query.take(ps.limit).getMany();
return await Pages.packMany(pages);
});
@@ -0,0 +1,58 @@
import { addPinned } from "../../../../services/i/pin.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { Users } from "../../../../models/index.js";
export const meta = {
tags: [
"account",
"notes"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "56734f8b-3928-431e-bf80-6ff87df40cb3"
},
pinLimitExceeded: {
message: "You can not pin notes any more.",
code: "PIN_LIMIT_EXCEEDED",
id: "72dab508-c64d-498f-8740-a8eec1ba385a"
},
alreadyPinned: {
message: "That note has already been pinned.",
code: "ALREADY_PINNED",
id: "8b18c2b7-68fe-4edb-9892-c0cbaeb6c913"
}
},
res: {
type: "object",
optional: false,
nullable: false,
ref: "MeDetailed"
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
await addPinned(user, ps.noteId).catch((e)=>{
if (e.id === "70c4e51f-5bea-449c-a030-53bee3cce202") throw new ApiError(meta.errors.noSuchNote);
if (e.id === "15a018eb-58e5-4da1-93be-330fcc5e4e1a") throw new ApiError(meta.errors.pinLimitExceeded);
if (e.id === "23f0cf4e-59a3-4276-a91d-61a5891c1514") throw new ApiError(meta.errors.alreadyPinned);
throw e;
});
return await Users.pack(user.id, user, {
detail: true
});
});
@@ -0,0 +1,38 @@
import { publishMainStream } from "../../../../services/stream.js";
import define from "../../define.js";
import { MessagingMessages, UserGroupJoinings } from "../../../../models/index.js";
export const meta = {
tags: [
"account",
"messaging"
],
requireCredential: true,
kind: "write:account"
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
// Update documents
await MessagingMessages.update({
recipientId: user.id,
isRead: false
}, {
isRead: true
});
const joinings = await UserGroupJoinings.findBy({
userId: user.id
});
await Promise.all(joinings.map((j)=>MessagingMessages.createQueryBuilder().update().set({
reads: ()=>`array_append("reads", '${user.id}')`
}).where("groupId = :groupId", {
groupId: j.userGroupId
}).andWhere("userId != :userId", {
userId: user.id
}).andWhere("NOT (:userId = ANY(reads))", {
userId: user.id
}).execute()));
publishMainStream(user.id, "readAllMessagingMessages");
});
@@ -0,0 +1,24 @@
import { publishMainStream } from "../../../../services/stream.js";
import define from "../../define.js";
import { NoteUnreads } from "../../../../models/index.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account"
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
// Remove documents
await NoteUnreads.delete({
userId: user.id
});
// 全て既読になったイベントを発行
publishMainStream(user.id, "readAllUnreadMentions");
publishMainStream(user.id, "readAllUnreadSpecifiedNotes");
});
@@ -0,0 +1,62 @@
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { genId } from "../../../../misc/gen-id.js";
import { AnnouncementReads, Announcements, Users } from "../../../../models/index.js";
import { publishMainStream } from "../../../../services/stream.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchAnnouncement: {
message: "No such announcement.",
code: "NO_SUCH_ANNOUNCEMENT",
id: "184663db-df88-4bc2-8b52-fb85f0681939"
}
}
};
export const paramDef = {
type: "object",
properties: {
announcementId: {
type: "string",
format: "misskey:id"
}
},
required: [
"announcementId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
// Check if announcement exists
const exist = await Announcements.exist({
where: {
id: ps.announcementId
}
});
if (!exist) {
throw new ApiError(meta.errors.noSuchAnnouncement);
}
// Check if already read
const read = await AnnouncementReads.exist({
where: {
announcementId: ps.announcementId,
userId: user.id
}
});
if (read) {
return;
}
// Create read
await AnnouncementReads.insert({
id: genId(),
createdAt: new Date(),
announcementId: ps.announcementId,
userId: user.id
});
if (!await Users.getHasUnreadAnnouncement(user.id)) {
publishMainStream(user.id, "readAllAnnouncements");
}
});
@@ -0,0 +1,49 @@
import { publishInternalEvent, publishMainStream, publishUserEvent } from "../../../../services/stream.js";
import generateUserToken from "../../common/generate-native-user-token.js";
import define from "../../define.js";
import { Users, UserProfiles } from "../../../../models/index.js";
import { comparePassword } from "../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const freshUser = await Users.findOneByOrFail({
id: user.id
});
const oldToken = freshUser.token;
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new Error("incorrect password");
}
const newToken = generateUserToken();
await Users.update(user.id, {
token: newToken
});
// Publish event
publishInternalEvent("userTokenRegenerated", {
id: user.id,
oldToken,
newToken
});
publishMainStream(user.id, "myTokenRegenerated");
// Terminate streaming
setTimeout(()=>{
publishUserEvent(user.id, "terminate", {});
}, 5000);
});
@@ -0,0 +1,33 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const items = await query.getMany();
const res = {};
for (const item of items){
res[item.key] = item.value;
}
return res;
});
@@ -0,0 +1,50 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
import { ApiError } from "../../../error.js";
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: "No such key.",
code: "NO_SUCH_KEY",
id: "97a1e8e7-c0f7-47d2-957a-92e61256e01a"
}
}
};
export const paramDef = {
type: "object",
properties: {
key: {
type: "string"
},
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: [
"key"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.key = :key", {
key: ps.key
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const item = await query.getOne();
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);
}
return {
updatedAt: item.updatedAt,
value: item.value
};
});
@@ -0,0 +1,47 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
import { ApiError } from "../../../error.js";
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: "No such key.",
code: "NO_SUCH_KEY",
id: "ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a"
}
}
};
export const paramDef = {
type: "object",
properties: {
key: {
type: "string"
},
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: [
"key"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.key = :key", {
key: ps.key
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const item = await query.getOne();
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);
}
return item.value;
});
@@ -0,0 +1,34 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const items = await query.getMany();
const res = {};
for (const item of items){
const type = typeof item.value;
res[item.key] = item.value === null ? "null" : Array.isArray(item.value) ? "array" : type === "number" ? "number" : type === "string" ? "string" : type === "boolean" ? "boolean" : type === "object" ? "object" : null;
}
return res;
});
@@ -0,0 +1,29 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").select("item.key").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const items = await query.getMany();
return items.map((x)=>x.key);
});
@@ -0,0 +1,47 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
import { ApiError } from "../../../error.js";
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: "No such key.",
code: "NO_SUCH_KEY",
id: "1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019"
}
}
};
export const paramDef = {
type: "object",
properties: {
key: {
type: "string"
},
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: [
"key"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.key = :key", {
key: ps.key
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const item = await query.getOne();
if (item == null) {
throw new ApiError(meta.errors.noSuchKey);
}
await RegistryItems.remove(item);
});
@@ -0,0 +1,23 @@
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").select("item.scope").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
});
const items = await query.getMany();
const res = [];
for (const item of items){
if (res.some((scope)=>scope.join(".") === item.scope.join("."))) continue;
res.push(item.scope);
}
return res;
});
@@ -0,0 +1,63 @@
import { publishMainStream } from "../../../../../services/stream.js";
import define from "../../../define.js";
import { RegistryItems } from "../../../../../models/index.js";
import { genId } from "../../../../../misc/gen-id.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
key: {
type: "string",
minLength: 1
},
value: {},
scope: {
type: "array",
default: [],
items: {
type: "string",
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1)
}
}
},
required: [
"key",
"value"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = RegistryItems.createQueryBuilder("item").where("item.domain IS NULL").andWhere("item.userId = :userId", {
userId: user.id
}).andWhere("item.key = :key", {
key: ps.key
}).andWhere("item.scope = :scope", {
scope: ps.scope
});
const existingItem = await query.getOne();
if (existingItem) {
await RegistryItems.update(existingItem.id, {
updatedAt: new Date(),
value: ps.value
});
} else {
await RegistryItems.insert({
id: genId(),
createdAt: new Date(),
updatedAt: new Date(),
userId: user.id,
domain: null,
scope: ps.scope,
key: ps.key,
value: ps.value
});
}
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
publishMainStream(user.id, "registryUpdated", {
scope: ps.scope,
key: ps.key,
value: ps.value
});
});
@@ -0,0 +1,46 @@
import define from "../../define.js";
import { VerifiedBadgeRequests, Users } from "../../../../models/index.js";
import { genId } from "../../../../misc/gen-id.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account"
};
export const paramDef = {
type: "object",
properties: {
comment: {
type: "string",
maxLength: 2048,
default: ""
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const me = await Users.findOneByOrFail({
id: user.id
});
if (me.isVerified) {
throw new Error("already verified");
}
const existing = await VerifiedBadgeRequests.findOneBy({
userId: user.id,
status: "pending"
});
if (existing != null) {
return await VerifiedBadgeRequests.pack(existing);
}
const request = await VerifiedBadgeRequests.insert({
id: genId(),
createdAt: new Date(),
resolvedAt: null,
userId: user.id,
resolverId: null,
status: "pending",
comment: ps.comment ?? ""
}).then((x)=>VerifiedBadgeRequests.findOneByOrFail(x.identifiers[0]));
return await VerifiedBadgeRequests.pack(request);
});
@@ -0,0 +1,34 @@
import define from "../../define.js";
import { AccessTokens } from "../../../../models/index.js";
import { publishUserEvent } from "../../../../services/stream.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
tokenId: {
type: "string",
format: "misskey:id"
}
},
required: [
"tokenId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const exist = await AccessTokens.exist({
where: {
id: ps.tokenId
}
});
if (exist) {
await AccessTokens.delete({
id: ps.tokenId,
userId: user.id
});
// Terminate streaming
publishUserEvent(user.id, "terminate");
}
});
@@ -0,0 +1,34 @@
import define from "../../define.js";
import { Signins } from "../../../../models/index.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
requireCredential: true,
secure: true
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(Signins.createQueryBuilder("signin"), ps.sinceId, ps.untilId).andWhere("signin.userId = :meId", {
meId: user.id
});
const history = await query.take(ps.limit).getMany();
return await Promise.all(history.map((record)=>Signins.pack(record)));
});
@@ -0,0 +1,46 @@
import { removePinned } from "../../../../services/i/pin.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { Users } from "../../../../models/index.js";
export const meta = {
tags: [
"account",
"notes"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "454170ce-9d63-4a43-9da1-ea10afe81e21"
}
},
res: {
type: "object",
optional: false,
nullable: false,
ref: "MeDetailed"
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
await removePinned(user, ps.noteId).catch((e)=>{
if (e.id === "b302d4cf-c050-400a-bbb3-be208681f40c") throw new ApiError(meta.errors.noSuchNote);
throw e;
});
return await Users.pack(user.id, user, {
detail: true
});
});
@@ -0,0 +1,81 @@
import { publishMainStream } from "../../../../services/stream.js";
import define from "../../define.js";
import rndstr from "rndstr";
import config from "../../../../config/index.js";
import { Users, UserProfiles } from "../../../../models/index.js";
import { sendEmail } from "../../../../services/send-email.js";
import { ApiError } from "../../error.js";
import { validateEmailForAccount } from "../../../../services/validate-email-for-account.js";
import { HOUR } from "../../../../const.js";
import { comparePassword } from "../../../../misc/password.js";
export const meta = {
requireCredential: true,
secure: true,
limit: {
duration: HOUR,
max: 3
},
errors: {
incorrectPassword: {
message: "Incorrect password.",
code: "INCORRECT_PASSWORD",
id: "e54c1d7e-e7d6-4103-86b6-0a95069b4ad3"
},
unavailable: {
message: "Unavailable email address.",
code: "UNAVAILABLE",
id: "a2defefb-f220-8849-0af6-17f816099323"
}
}
};
export const paramDef = {
type: "object",
properties: {
password: {
type: "string"
},
email: {
type: "string",
nullable: true
}
},
required: [
"password"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
// Compare password
const same = await comparePassword(ps.password, profile.password);
if (!same) {
throw new ApiError(meta.errors.incorrectPassword);
}
if (ps.email != null) {
const available = await validateEmailForAccount(ps.email);
if (!available) {
throw new ApiError(meta.errors.unavailable);
}
}
await UserProfiles.update(user.id, {
email: ps.email,
emailVerified: false,
emailVerifyCode: null
});
const iObj = await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
});
// Publish meUpdated event
publishMainStream(user.id, "meUpdated", iObj);
if (ps.email != null) {
const code = rndstr("a-z0-9", 16);
await UserProfiles.update(user.id, {
emailVerifyCode: code
});
const link = `${config.url}/verify-email/${code}`;
sendEmail(ps.email, "Email verification", `To verify email, please click this link:<br><a href="${link}">${link}</a>`, `To verify email, please click this link: ${link}`);
}
return iObj;
});
@@ -0,0 +1,349 @@
import RE2 from "re2";
import { updateUserProfileData } from "../../../../services/i/update.js";
import { Users, DriveFiles, UserProfiles, Pages } from "../../../../models/index.js";
import { notificationTypes } from "../../../../types.js";
import { langmap } from "../../../../misc/langmap.js";
import { verifyLink } from "../../../../services/fetch-rel-me.js";
import { ApiError } from "../../error.js";
import define from "../../define.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchAvatar: {
message: "No such avatar file.",
code: "NO_SUCH_AVATAR",
id: "539f3a45-f215-4f81-a9a8-31293640207f"
},
noSuchBanner: {
message: "No such banner file.",
code: "NO_SUCH_BANNER",
id: "0d8f5629-f210-41c2-9433-735831a58595"
},
avatarNotAnImage: {
message: "The file specified as an avatar is not an image.",
code: "AVATAR_NOT_AN_IMAGE",
id: "f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191"
},
bannerNotAnImage: {
message: "The file specified as a banner is not an image.",
code: "BANNER_NOT_AN_IMAGE",
id: "75aedb19-2afd-4e6d-87fc-67941256fa60"
},
noSuchPage: {
message: "No such page.",
code: "NO_SUCH_PAGE",
id: "8e01b590-7eb9-431b-a239-860e086c408e"
},
invalidRegexp: {
message: "Invalid Regular Expression.",
code: "INVALID_REGEXP",
id: "0d786918-10df-41cd-8f33-8dec7d9a89a5"
},
invalidFieldName: {
message: "Invalid field name.",
code: "INVALID_FIELD_NAME",
id: "8f81972e-8b53-4d30-b0d2-efb026dda673"
},
invalidFieldValue: {
message: "Invalid field value.",
code: "INVALID_FIELD_VALUE",
id: "aede7444-244b-11ee-be56-0242ac120002"
}
},
res: {
type: "object",
optional: false,
nullable: false,
ref: "MeDetailed"
}
};
export const paramDef = {
type: "object",
properties: {
name: {
...Users.nameSchema,
nullable: true
},
description: {
...Users.descriptionSchema,
nullable: true
},
location: {
...Users.locationSchema,
nullable: true
},
birthday: {
...Users.birthdaySchema,
nullable: true
},
lang: {
type: "string",
enum: [
null,
...Object.keys(langmap)
],
nullable: true
},
avatarId: {
type: "string",
format: "misskey:id",
nullable: true
},
bannerId: {
type: "string",
format: "misskey:id",
nullable: true
},
fields: {
type: "array",
minItems: 0,
maxItems: 16,
items: {
type: "object",
properties: {
name: {
type: "string"
},
value: {
type: "string"
}
},
required: [
"name",
"value"
]
}
},
isLocked: {
type: "boolean"
},
isExplorable: {
type: "boolean"
},
hideOnlineStatus: {
type: "boolean"
},
publicReactions: {
type: "boolean"
},
allowCalls: {
type: "boolean"
},
symbolFileId: {
type: "string",
format: "misskey:id",
nullable: true
},
carefulBot: {
type: "boolean"
},
autoAcceptFollowed: {
type: "boolean"
},
noCrawle: {
type: "boolean"
},
preventAiLearning: {
type: "boolean"
},
isBot: {
type: "boolean"
},
isCat: {
type: "boolean"
},
speakAsCat: {
type: "boolean"
},
minorBadges: {
type: "array",
uniqueItems: true,
maxItems: 1,
items: {
type: "string",
enum: [
"K",
"T",
"E"
]
}
},
injectFeaturedNote: {
type: "boolean"
},
receiveAnnouncementEmail: {
type: "boolean"
},
alwaysMarkNsfw: {
type: "boolean"
},
ffVisibility: {
type: "string",
enum: [
"public",
"followers",
"private"
]
},
pinnedPageId: {
type: "string",
format: "misskey:id",
nullable: true
},
mutedWords: {
type: "array"
},
mutedInstances: {
type: "array",
items: {
type: "string"
}
},
mutingNotificationTypes: {
type: "array",
items: {
type: "string",
enum: notificationTypes
}
},
emailNotificationTypes: {
type: "array",
items: {
type: "string"
}
},
pronouns: {
type: "object",
nullable: true
},
canBite: {
type: "string",
enum: [
"anyone",
"followers",
"nobody"
],
nullable: true
}
}
};
export default define(meta, paramDef, async (ps, _user, token)=>{
const user = await Users.findOneByOrFail({
id: _user.id
});
const isSecure = token == null;
const updates = {};
const profileUpdates = {};
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (ps.mutedWords !== undefined) {
// validate regular expression syntax
ps.mutedWords.filter((x)=>!Array.isArray(x)).forEach((x)=>{
const regexp = x.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
new RE2(regexp[1], regexp[2]);
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
});
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes;
if (typeof ps.isLocked === "boolean") updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === "boolean") updates.isExplorable = ps.isExplorable;
if (typeof ps.hideOnlineStatus === "boolean") updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.publicReactions === "boolean") profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.allowCalls === "boolean") profileUpdates.allowCalls = ps.allowCalls;
if (ps.symbolFileId !== undefined) profileUpdates.symbolFileId = ps.symbolFileId;
if (typeof ps.isBot === "boolean") updates.isBot = ps.isBot;
if (ps.minorBadges !== undefined) updates.minorBadges = ps.minorBadges.slice(0, 1);
if (typeof ps.carefulBot === "boolean") profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === "boolean") profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === "boolean") profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === "boolean") profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === "boolean") profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === "boolean") profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (typeof ps.pronouns === "object") {
if (ps.pronouns === null) ps.pronouns = {};
for (const key of Object.keys(ps.pronouns)){
if (key.length !== 2 || typeof ps.pronouns[key] !== "string" || ps.pronouns[key].length === 0) {
delete ps.pronouns[key];
}
}
profileUpdates.pronouns = ps.pronouns;
}
const avatar = ps.avatarId ? await DriveFiles.findOneBy({
id: ps.avatarId
}) : null;
const banner = ps.bannerId ? await DriveFiles.findOneBy({
id: ps.bannerId
}) : null;
if (ps.avatarId) {
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.type.startsWith("image/")) throw new ApiError(meta.errors.avatarNotAnImage);
updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true);
updates.avatarBlurhash = avatar.blurhash;
}
if (ps.bannerId) {
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith("image/")) throw new ApiError(meta.errors.bannerNotAnImage);
updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false);
updates.bannerBlurhash = banner.blurhash;
}
if (ps.pinnedPageId) {
const page = await Pages.findOneBy({
id: ps.pinnedPageId
});
if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
profileUpdates.pinnedPageId = page.id;
} else if (ps.pinnedPageId === null) {
profileUpdates.pinnedPageId = null;
}
if (ps.fields) {
for (const field of ps.fields){
if (!field || field.name === "" || field.value === "") {
continue;
}
if (typeof field.name !== "string" || field.name === "") {
throw new ApiError(meta.errors.invalidFieldName);
}
if (typeof field.value !== "string" || field.value === "") {
throw new ApiError(meta.errors.invalidFieldValue);
}
if (field.value.startsWith("http")) {
field.verified = await verifyLink(field.value, user.username);
}
}
profileUpdates.fields = ps.fields.filter((x)=>Object.keys(x).length !== 0).map((x)=>{
return {
name: x.name,
value: x.value,
verified: x.verified
};
});
}
if (ps.canBite) {
updates.canBite = ps.canBite;
}
return updateUserProfileData(user, profile, updates, profileUpdates, isSecure);
});
@@ -0,0 +1,96 @@
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { DriveFiles, UserEmojis } from "../../../../../models/index.js";
import { genId } from "../../../../../misc/gen-id.js";
import { getEmojiSize } from "../../../../../misc/emoji-meta.js";
import { clearUserEmojiCache } from "../../../../../misc/populate-emojis.js";
function normalizeMimeType(type) {
const mime = type?.split(";")[0]?.trim().toLowerCase();
return mime && mime.length <= 64 ? mime : null;
}
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "7be656a7-cb71-41a8-9904-e8b6481e61dd"
},
notImage: {
message: "The file is not an image.",
code: "NOT_IMAGE",
id: "64cfbb24-2628-4217-873d-09e22957cf49"
},
alreadyExists: {
message: "User emoji already exists.",
code: "ALREADY_EXISTS",
id: "259b6fdb-c603-4244-81cb-7218b967ab62"
}
}
};
export const paramDef = {
type: "object",
properties: {
name: {
type: "string",
pattern: "^[a-z0-9_]{1,64}$"
},
fileId: {
type: "string",
format: "misskey:id"
},
glyph: {
type: "boolean",
default: false
}
},
required: [
"name",
"fileId"
]
};
export default define(meta, paramDef, async (ps, me)=>{
const file = await DriveFiles.findOneBy({
id: ps.fileId,
userId: me.id
});
if (!file) throw new ApiError(meta.errors.noSuchFile);
if (!file.type.startsWith("image/")) throw new ApiError(meta.errors.notImage);
const exists = await UserEmojis.findOneBy({
name: ps.name,
userId: me.id
});
if (exists) throw new ApiError(meta.errors.alreadyExists);
const size = await getEmojiSize(file.url).catch(()=>({
width: null,
height: null
}));
const type = normalizeMimeType(file.webpublicType ?? file.type);
const emoji = await UserEmojis.insert({
id: genId(),
createdAt: new Date(),
name: ps.name,
userId: me.id,
userGroupId: null,
originalUrl: file.url,
publicUrl: file.webpublicUrl ?? file.url,
type,
glyph: ps.glyph,
width: size.width || null,
height: size.height || null
}).then((x)=>UserEmojis.findOneByOrFail(x.identifiers[0]));
await clearUserEmojiCache(emoji.name, me.username, me.host);
return {
id: emoji.id,
name: emoji.name,
url: emoji.publicUrl || emoji.originalUrl,
glyph: emoji.glyph,
glyphUrl: emoji.glyph ? emoji.originalUrl : null,
width: emoji.width,
height: emoji.height
};
});
@@ -0,0 +1,39 @@
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { UserEmojis } from "../../../../../models/index.js";
import { clearUserEmojiCache } from "../../../../../misc/populate-emojis.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchEmoji: {
message: "No such user emoji.",
code: "NO_SUCH_USER_EMOJI",
id: "a44ed453-7bec-4ae0-940e-2eb3e80e7b62"
}
}
};
export const paramDef = {
type: "object",
properties: {
id: {
type: "string",
format: "misskey:id"
}
},
required: [
"id"
]
};
export default define(meta, paramDef, async (ps, me)=>{
const emoji = await UserEmojis.findOneBy({
id: ps.id,
userId: me.id
});
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
await UserEmojis.delete(emoji.id);
await clearUserEmojiCache(emoji.name, me.username, me.host);
});
@@ -0,0 +1,33 @@
import define from "../../../define.js";
import { UserEmojis } from "../../../../../models/index.js";
export const meta = {
tags: [
"account"
],
requireCredential: true,
kind: "read:account"
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (_ps, me)=>{
const emojis = await UserEmojis.find({
where: {
userId: me.id
},
order: {
createdAt: "DESC"
}
});
return emojis.map((emoji)=>({
id: emoji.id,
name: emoji.name,
url: emoji.publicUrl || emoji.originalUrl,
glyph: emoji.glyph,
glyphUrl: emoji.glyph ? emoji.originalUrl : null,
width: emoji.width,
height: emoji.height
}));
});
@@ -0,0 +1,62 @@
import define from "../../define.js";
import { UserGroupInvitations } from "../../../../models/index.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
export const meta = {
tags: [
"account",
"groups"
],
requireCredential: true,
kind: "read:user-groups",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id"
},
group: {
type: "object",
optional: false,
nullable: false,
ref: "UserGroup"
}
}
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder("invitation"), ps.sinceId, ps.untilId).andWhere("invitation.userId = :meId", {
meId: user.id
}).leftJoinAndSelect("invitation.userGroup", "user_group");
const invitations = await query.take(ps.limit).getMany();
return await UserGroupInvitations.packMany(invitations);
});
@@ -0,0 +1,58 @@
import define from "../../../define.js";
import { genId } from "../../../../../misc/gen-id.js";
import { Webhooks } from "../../../../../models/index.js";
import { publishInternalEvent } from "../../../../../services/stream.js";
import { webhookEventTypes } from "../../../../../models/entities/webhook.js";
export const meta = {
tags: [
"webhooks"
],
requireCredential: true,
kind: "write:account"
};
export const paramDef = {
type: "object",
properties: {
name: {
type: "string",
minLength: 1,
maxLength: 100
},
url: {
type: "string",
minLength: 1,
maxLength: 1024
},
secret: {
type: "string",
minLength: 1,
maxLength: 1024
},
on: {
type: "array",
items: {
type: "string",
enum: webhookEventTypes
}
}
},
required: [
"name",
"url",
"secret",
"on"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const webhook = await Webhooks.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on
}).then((x)=>Webhooks.findOneByOrFail(x.identifiers[0]));
publishInternalEvent("webhookCreated", webhook);
return webhook;
});
@@ -0,0 +1,41 @@
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { Webhooks } from "../../../../../models/index.js";
import { publishInternalEvent } from "../../../../../services/stream.js";
export const meta = {
tags: [
"webhooks"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchWebhook: {
message: "No such webhook.",
code: "NO_SUCH_WEBHOOK",
id: "bae73e5a-5522-4965-ae19-3a8688e71d82"
}
}
};
export const paramDef = {
type: "object",
properties: {
webhookId: {
type: "string",
format: "misskey:id"
}
},
required: [
"webhookId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
await Webhooks.delete(webhook.id);
publishInternalEvent("webhookDeleted", webhook);
});
@@ -0,0 +1,21 @@
import define from "../../../define.js";
import { Webhooks } from "../../../../../models/index.js";
export const meta = {
tags: [
"webhooks",
"account"
],
requireCredential: true,
kind: "read:account"
};
export const paramDef = {
type: "object",
properties: {},
required: []
};
export default define(meta, paramDef, async (ps, me)=>{
const webhooks = await Webhooks.findBy({
userId: me.id
});
return webhooks;
});
@@ -0,0 +1,39 @@
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { Webhooks } from "../../../../../models/index.js";
export const meta = {
tags: [
"webhooks"
],
requireCredential: true,
kind: "read:account",
errors: {
noSuchWebhook: {
message: "No such webhook.",
code: "NO_SUCH_WEBHOOK",
id: "50f614d9-3047-4f7e-90d8-ad6b2d5fb098"
}
}
};
export const paramDef = {
type: "object",
properties: {
webhookId: {
type: "string",
format: "misskey:id"
}
},
required: [
"webhookId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
return webhook;
});
@@ -0,0 +1,78 @@
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { Webhooks } from "../../../../../models/index.js";
import { publishInternalEvent } from "../../../../../services/stream.js";
import { webhookEventTypes } from "../../../../../models/entities/webhook.js";
export const meta = {
tags: [
"webhooks"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchWebhook: {
message: "No such webhook.",
code: "NO_SUCH_WEBHOOK",
id: "fb0fea69-da18-45b1-828d-bd4fd1612518"
}
}
};
export const paramDef = {
type: "object",
properties: {
webhookId: {
type: "string",
format: "misskey:id"
},
name: {
type: "string",
minLength: 1,
maxLength: 100
},
url: {
type: "string",
minLength: 1,
maxLength: 1024
},
secret: {
type: "string",
minLength: 1,
maxLength: 1024
},
on: {
type: "array",
items: {
type: "string",
enum: webhookEventTypes
}
},
active: {
type: "boolean"
}
},
required: [
"webhookId",
"name",
"url",
"secret",
"on",
"active"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
await Webhooks.update(webhook.id, {
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
active: ps.active
});
publishInternalEvent("webhookUpdated", webhook);
});