parent
65ed702d87
commit
562b02310f
24 changed files with 6 additions and 1426 deletions
|
@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import type { UtilityService } from '@/core/UtilityService.js';
|
import type { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
|
|
||||||
const services: {
|
|
||||||
[x: string]: (id: string, username: string) => any
|
|
||||||
} = {
|
|
||||||
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
|
|
||||||
'misskey:authentication:github': (id, login) => ({ id, login }),
|
|
||||||
'misskey:authentication:discord': (id, name) => $discord(id, name),
|
|
||||||
};
|
|
||||||
|
|
||||||
const $discord = (id: string, name: string) => {
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
name = 'unknown#0000';
|
|
||||||
}
|
|
||||||
const [username, discriminator] = name.split('#');
|
|
||||||
return { id, username, discriminator };
|
|
||||||
};
|
|
||||||
|
|
||||||
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
|
|
||||||
const service = services[source.name];
|
|
||||||
|
|
||||||
if (typeof source.value !== 'string') {
|
|
||||||
source.value = 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const [id, username] = source.value.split('@');
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
target[source.name.split(':')[2]] = service(id, username);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApPersonService implements OnModuleInit {
|
export class ApPersonService implements OnModuleInit {
|
||||||
private utilityService: UtilityService;
|
private utilityService: UtilityService;
|
||||||
|
@ -540,22 +510,16 @@ export class ApPersonService implements OnModuleInit {
|
||||||
name: string,
|
name: string,
|
||||||
value: string
|
value: string
|
||||||
}[] = [];
|
}[] = [];
|
||||||
const services: { [x: string]: any } = {};
|
|
||||||
|
|
||||||
if (Array.isArray(attachments)) {
|
if (Array.isArray(attachments)) {
|
||||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||||
if (isPropertyValue(attachment.identifier)) {
|
fields.push({
|
||||||
addService(services, attachment.identifier);
|
name: attachment.name,
|
||||||
} else {
|
value: this.mfmService.fromHtml(attachment.value),
|
||||||
fields.push({
|
});
|
||||||
name: attachment.name,
|
|
||||||
value: this.mfmService.fromHtml(attachment.value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { fields, services };
|
return { fields };
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -489,7 +489,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
integrations: profile!.integrations,
|
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||||
|
|
|
@ -279,57 +279,6 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public swPrivateKey: string | null;
|
public swPrivateKey: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public enableTwitterIntegration: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public twitterConsumerKey: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public twitterConsumerSecret: string | null;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public enableGithubIntegration: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public githubClientId: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public githubClientSecret: string | null;
|
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
public enableDiscordIntegration: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public discordClientId: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public discordClientSecret: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 128,
|
length: 128,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -184,11 +184,6 @@ export class UserProfile {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public pinnedPage: Page | null;
|
public pinnedPage: Page | null;
|
||||||
|
|
||||||
@Column('jsonb', {
|
|
||||||
default: {},
|
|
||||||
})
|
|
||||||
public integrations: Record<string, any>;
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false, select: false,
|
default: false, select: false,
|
||||||
|
|
|
@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
integrations: {
|
|
||||||
type: 'object',
|
|
||||||
nullable: true, optional: false,
|
|
||||||
},
|
|
||||||
mutedWords: {
|
mutedWords: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
|
|
@ -111,9 +111,6 @@ export class NodeinfoServerService {
|
||||||
enableHcaptcha: meta.enableHcaptcha,
|
enableHcaptcha: meta.enableHcaptcha,
|
||||||
enableRecaptcha: meta.enableRecaptcha,
|
enableRecaptcha: meta.enableRecaptcha,
|
||||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||||
enableTwitterIntegration: meta.enableTwitterIntegration,
|
|
||||||
enableGithubIntegration: meta.enableGithubIntegration,
|
|
||||||
enableDiscordIntegration: meta.enableDiscordIntegration,
|
|
||||||
enableEmail: meta.enableEmail,
|
enableEmail: meta.enableEmail,
|
||||||
enableServiceWorker: meta.enableServiceWorker,
|
enableServiceWorker: meta.enableServiceWorker,
|
||||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||||
|
|
|
@ -7,9 +7,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
import { ServerService } from './ServerService.js';
|
import { ServerService } from './ServerService.js';
|
||||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
import { GetterService } from './api/GetterService.js';
|
import { GetterService } from './api/GetterService.js';
|
||||||
import { DiscordServerService } from './api/integration/DiscordServerService.js';
|
|
||||||
import { GithubServerService } from './api/integration/GithubServerService.js';
|
|
||||||
import { TwitterServerService } from './api/integration/TwitterServerService.js';
|
|
||||||
import { ChannelsService } from './api/stream/ChannelsService.js';
|
import { ChannelsService } from './api/stream/ChannelsService.js';
|
||||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||||
import { ApiLoggerService } from './api/ApiLoggerService.js';
|
import { ApiLoggerService } from './api/ApiLoggerService.js';
|
||||||
|
@ -54,9 +51,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||||
ServerService,
|
ServerService,
|
||||||
WellKnownServerService,
|
WellKnownServerService,
|
||||||
GetterService,
|
GetterService,
|
||||||
DiscordServerService,
|
|
||||||
GithubServerService,
|
|
||||||
TwitterServerService,
|
|
||||||
ChannelsService,
|
ChannelsService,
|
||||||
ApiCallService,
|
ApiCallService,
|
||||||
ApiLoggerService,
|
ApiLoggerService,
|
||||||
|
|
|
@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
|
||||||
import { ApiCallService } from './ApiCallService.js';
|
import { ApiCallService } from './ApiCallService.js';
|
||||||
import { SignupApiService } from './SignupApiService.js';
|
import { SignupApiService } from './SignupApiService.js';
|
||||||
import { SigninApiService } from './SigninApiService.js';
|
import { SigninApiService } from './SigninApiService.js';
|
||||||
import { GithubServerService } from './integration/GithubServerService.js';
|
|
||||||
import { DiscordServerService } from './integration/DiscordServerService.js';
|
|
||||||
import { TwitterServerService } from './integration/TwitterServerService.js';
|
|
||||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -38,9 +35,6 @@ export class ApiServerService {
|
||||||
private apiCallService: ApiCallService,
|
private apiCallService: ApiCallService,
|
||||||
private signupApiService: SignupApiService,
|
private signupApiService: SignupApiService,
|
||||||
private signinApiService: SigninApiService,
|
private signinApiService: SigninApiService,
|
||||||
private githubServerService: GithubServerService,
|
|
||||||
private discordServerService: DiscordServerService,
|
|
||||||
private twitterServerService: TwitterServerService,
|
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -133,10 +127,6 @@ export class ApiServerService {
|
||||||
|
|
||||||
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
|
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
|
||||||
|
|
||||||
fastify.register(this.discordServerService.create);
|
|
||||||
fastify.register(this.githubServerService.create);
|
|
||||||
fastify.register(this.twitterServerService.create);
|
|
||||||
|
|
||||||
fastify.get('/v1/instance/peers', async (request, reply) => {
|
fastify.get('/v1/instance/peers', async (request, reply) => {
|
||||||
const instances = await this.instancesRepository.find({
|
const instances = await this.instancesRepository.find({
|
||||||
select: ['host'],
|
select: ['host'],
|
||||||
|
|
|
@ -138,18 +138,6 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableTwitterIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableGithubIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableDiscordIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableServiceWorker: {
|
enableServiceWorker: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -223,30 +211,6 @@ export const meta = {
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
twitterConsumerKey: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
twitterConsumerSecret: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
githubClientId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
githubClientSecret: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
discordClientId: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
discordClientSecret: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
summaryProxy: {
|
summaryProxy: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -389,9 +353,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
defaultLightTheme: instance.defaultLightTheme,
|
defaultLightTheme: instance.defaultLightTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme,
|
defaultDarkTheme: instance.defaultDarkTheme,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
|
||||||
enableGithubIntegration: instance.enableGithubIntegration,
|
|
||||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable: instance.deeplAuthKey != null,
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedPages: instance.pinnedPages,
|
||||||
|
@ -409,12 +370,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||||
proxyAccountId: instance.proxyAccountId,
|
proxyAccountId: instance.proxyAccountId,
|
||||||
twitterConsumerKey: instance.twitterConsumerKey,
|
|
||||||
twitterConsumerSecret: instance.twitterConsumerSecret,
|
|
||||||
githubClientId: instance.githubClientId,
|
|
||||||
githubClientSecret: instance.githubClientSecret,
|
|
||||||
discordClientId: instance.discordClientId,
|
|
||||||
discordClientSecret: instance.discordClientSecret,
|
|
||||||
summalyProxy: instance.summalyProxy,
|
summalyProxy: instance.summalyProxy,
|
||||||
email: instance.email,
|
email: instance.email,
|
||||||
smtpSecure: instance.smtpSecure,
|
smtpSecure: instance.smtpSecure,
|
||||||
|
|
|
@ -65,11 +65,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
|
|
||||||
Object.keys(profile.integrations).forEach(integration => {
|
|
||||||
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
|
|
||||||
});
|
|
||||||
|
|
||||||
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
||||||
|
|
||||||
const roles = await this.roleService.getUserRoles(user.id);
|
const roles = await this.roleService.getUserRoles(user.id);
|
||||||
|
@ -84,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
carefulBot: profile.carefulBot,
|
carefulBot: profile.carefulBot,
|
||||||
injectFeaturedNote: profile.injectFeaturedNote,
|
injectFeaturedNote: profile.injectFeaturedNote,
|
||||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||||
integrations: profile.integrations,
|
|
||||||
mutedWords: profile.mutedWords,
|
mutedWords: profile.mutedWords,
|
||||||
mutedInstances: profile.mutedInstances,
|
mutedInstances: profile.mutedInstances,
|
||||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||||
|
|
|
@ -68,15 +68,6 @@ export const paramDef = {
|
||||||
summalyProxy: { type: 'string', nullable: true },
|
summalyProxy: { type: 'string', nullable: true },
|
||||||
deeplAuthKey: { type: 'string', nullable: true },
|
deeplAuthKey: { type: 'string', nullable: true },
|
||||||
deeplIsPro: { type: 'boolean' },
|
deeplIsPro: { type: 'boolean' },
|
||||||
enableTwitterIntegration: { type: 'boolean' },
|
|
||||||
twitterConsumerKey: { type: 'string', nullable: true },
|
|
||||||
twitterConsumerSecret: { type: 'string', nullable: true },
|
|
||||||
enableGithubIntegration: { type: 'boolean' },
|
|
||||||
githubClientId: { type: 'string', nullable: true },
|
|
||||||
githubClientSecret: { type: 'string', nullable: true },
|
|
||||||
enableDiscordIntegration: { type: 'boolean' },
|
|
||||||
discordClientId: { type: 'string', nullable: true },
|
|
||||||
discordClientSecret: { type: 'string', nullable: true },
|
|
||||||
enableEmail: { type: 'boolean' },
|
enableEmail: { type: 'boolean' },
|
||||||
email: { type: 'string', nullable: true },
|
email: { type: 'string', nullable: true },
|
||||||
smtpSecure: { type: 'boolean' },
|
smtpSecure: { type: 'boolean' },
|
||||||
|
@ -270,42 +261,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
set.summalyProxy = ps.summalyProxy;
|
set.summalyProxy = ps.summalyProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.enableTwitterIntegration !== undefined) {
|
|
||||||
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.twitterConsumerKey !== undefined) {
|
|
||||||
set.twitterConsumerKey = ps.twitterConsumerKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.twitterConsumerSecret !== undefined) {
|
|
||||||
set.twitterConsumerSecret = ps.twitterConsumerSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.enableGithubIntegration !== undefined) {
|
|
||||||
set.enableGithubIntegration = ps.enableGithubIntegration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.githubClientId !== undefined) {
|
|
||||||
set.githubClientId = ps.githubClientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.githubClientSecret !== undefined) {
|
|
||||||
set.githubClientSecret = ps.githubClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.enableDiscordIntegration !== undefined) {
|
|
||||||
set.enableDiscordIntegration = ps.enableDiscordIntegration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.discordClientId !== undefined) {
|
|
||||||
set.discordClientId = ps.discordClientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.discordClientSecret !== undefined) {
|
|
||||||
set.discordClientSecret = ps.discordClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ps.enableEmail !== undefined) {
|
if (ps.enableEmail !== undefined) {
|
||||||
set.enableEmail = ps.enableEmail;
|
set.enableEmail = ps.enableEmail;
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,18 +169,6 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
enableTwitterIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableGithubIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableDiscordIntegration: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
enableServiceWorker: {
|
enableServiceWorker: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -225,18 +213,6 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
twitter: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
discord: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
serviceWorker: {
|
serviceWorker: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -325,11 +301,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
})),
|
})),
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
|
||||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
|
||||||
enableGithubIntegration: instance.enableGithubIntegration,
|
|
||||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
|
||||||
|
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
|
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable: instance.deeplAuthKey != null,
|
||||||
|
@ -358,9 +329,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
recaptcha: instance.enableRecaptcha,
|
recaptcha: instance.enableRecaptcha,
|
||||||
turnstile: instance.enableTurnstile,
|
turnstile: instance.enableTurnstile,
|
||||||
objectStorage: instance.useObjectStorage,
|
objectStorage: instance.useObjectStorage,
|
||||||
twitter: instance.enableTwitterIntegration,
|
|
||||||
github: instance.enableGithubIntegration,
|
|
||||||
discord: instance.enableDiscordIntegration,
|
|
||||||
serviceWorker: instance.enableServiceWorker,
|
serviceWorker: instance.enableServiceWorker,
|
||||||
miauth: true,
|
miauth: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,308 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { OAuth2 } from 'oauth';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
|
||||||
import type { ILocalUser } from '@/models/entities/User.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import { SigninService } from '../SigninService.js';
|
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DiscordServerService {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.config)
|
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private httpRequestService: HttpRequestService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private metaService: MetaService,
|
|
||||||
private signinService: SigninService,
|
|
||||||
) {
|
|
||||||
//this.create = this.create.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
|
||||||
fastify.get('/disconnect/discord', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (!userToken) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
delete profile.integrations.discord;
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: profile.integrations,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return 'Discordの連携を解除しました :v:';
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOAuth2 = async () => {
|
|
||||||
const meta = await this.metaService.fetch(true);
|
|
||||||
|
|
||||||
if (meta.enableDiscordIntegration) {
|
|
||||||
return new OAuth2(
|
|
||||||
meta.discordClientId!,
|
|
||||||
meta.discordClientSecret!,
|
|
||||||
'https://discord.com/',
|
|
||||||
'api/oauth2/authorize',
|
|
||||||
'api/oauth2/token');
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fastify.get('/connect/discord', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (!userToken) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
redirect_uri: `${this.config.url}/api/dc/cb`,
|
|
||||||
scope: ['identify'],
|
|
||||||
state: uuid(),
|
|
||||||
response_type: 'code',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.redisClient.set(userToken, JSON.stringify(params));
|
|
||||||
|
|
||||||
const oauth2 = await getOAuth2();
|
|
||||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/signin/discord', async (request, reply) => {
|
|
||||||
const sessid = uuid();
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
redirect_uri: `${this.config.url}/api/dc/cb`,
|
|
||||||
scope: ['identify'],
|
|
||||||
state: uuid(),
|
|
||||||
response_type: 'code',
|
|
||||||
};
|
|
||||||
|
|
||||||
reply.setCookie('signin_with_discord_sid', sessid, {
|
|
||||||
path: '/',
|
|
||||||
secure: this.config.url.startsWith('https'),
|
|
||||||
httpOnly: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.redisClient.set(sessid, JSON.stringify(params));
|
|
||||||
|
|
||||||
const oauth2 = await getOAuth2();
|
|
||||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get<{ Querystring: { code: string; state: string; } }>('/dc/cb', async (request, reply) => {
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
|
|
||||||
const oauth2 = await getOAuth2();
|
|
||||||
|
|
||||||
if (!userToken) {
|
|
||||||
const sessid = request.cookies['signin_with_discord_sid'];
|
|
||||||
|
|
||||||
if (!sessid) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = request.query.code;
|
|
||||||
|
|
||||||
if (!code || typeof code !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(sessid, async (_, state) => {
|
|
||||||
if (state == null) throw new Error('empty state');
|
|
||||||
res(JSON.parse(state));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.query.state !== state) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
|
||||||
oauth2!.getOAuthAccessToken(code, {
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
redirect_uri,
|
|
||||||
}, (err, accessToken, refreshToken, result) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err);
|
|
||||||
} else if (result.error) {
|
|
||||||
rej(result.error);
|
|
||||||
} else {
|
|
||||||
res({
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
|
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
|
|
||||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.createQueryBuilder()
|
|
||||||
.where('"integrations"->\'discord\'->>\'id\' = :id', { id: id })
|
|
||||||
.andWhere('"userHost" IS NULL')
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (profile == null) {
|
|
||||||
throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(profile.userId, {
|
|
||||||
integrations: {
|
|
||||||
...profile.integrations,
|
|
||||||
discord: {
|
|
||||||
id: id,
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
expiresDate: expiresDate,
|
|
||||||
username: username,
|
|
||||||
discriminator: discriminator,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
|
|
||||||
} else {
|
|
||||||
const code = request.query.code;
|
|
||||||
|
|
||||||
if (!code || typeof code !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(userToken, async (_, state) => {
|
|
||||||
if (state == null) throw new Error('empty state');
|
|
||||||
res(JSON.parse(state));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.query.state !== state) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
|
|
||||||
oauth2!.getOAuthAccessToken(code, {
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
redirect_uri,
|
|
||||||
}, (err, accessToken, refreshToken, result) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err);
|
|
||||||
} else if (result.error) {
|
|
||||||
rej(result.error);
|
|
||||||
} else {
|
|
||||||
res({
|
|
||||||
accessToken,
|
|
||||||
refreshToken,
|
|
||||||
expiresDate: Date.now() + Number(result.expires_in) * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
|
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: {
|
|
||||||
...profile.integrations,
|
|
||||||
discord: {
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
expiresDate: expiresDate,
|
|
||||||
id: id,
|
|
||||||
username: username,
|
|
||||||
discriminator: discriminator,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private getUserToken(request: FastifyRequest): string | null {
|
|
||||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private compareOrigin(request: FastifyRequest): boolean {
|
|
||||||
function normalizeUrl(url?: string): string {
|
|
||||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const referer = request.headers['referer'];
|
|
||||||
|
|
||||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,280 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { OAuth2 } from 'oauth';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
|
||||||
import type { ILocalUser } from '@/models/entities/User.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import { SigninService } from '../SigninService.js';
|
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GithubServerService {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.config)
|
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private httpRequestService: HttpRequestService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private metaService: MetaService,
|
|
||||||
private signinService: SigninService,
|
|
||||||
) {
|
|
||||||
//this.create = this.create.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
|
||||||
fastify.get('/disconnect/github', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (!userToken) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
delete profile.integrations.github;
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: profile.integrations,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return 'GitHubの連携を解除しました :v:';
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOath2 = async () => {
|
|
||||||
const meta = await this.metaService.fetch(true);
|
|
||||||
|
|
||||||
if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
|
|
||||||
return new OAuth2(
|
|
||||||
meta.githubClientId,
|
|
||||||
meta.githubClientSecret,
|
|
||||||
'https://github.com/',
|
|
||||||
'login/oauth/authorize',
|
|
||||||
'login/oauth/access_token');
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fastify.get('/connect/github', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (!userToken) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
redirect_uri: `${this.config.url}/api/gh/cb`,
|
|
||||||
scope: ['read:user'],
|
|
||||||
state: uuid(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.redisClient.set(userToken, JSON.stringify(params));
|
|
||||||
|
|
||||||
const oauth2 = await getOath2();
|
|
||||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/signin/github', async (request, reply) => {
|
|
||||||
const sessid = uuid();
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
redirect_uri: `${this.config.url}/api/gh/cb`,
|
|
||||||
scope: ['read:user'],
|
|
||||||
state: uuid(),
|
|
||||||
};
|
|
||||||
|
|
||||||
reply.setCookie('signin_with_github_sid', sessid, {
|
|
||||||
path: '/',
|
|
||||||
secure: this.config.url.startsWith('https'),
|
|
||||||
httpOnly: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.redisClient.set(sessid, JSON.stringify(params));
|
|
||||||
|
|
||||||
const oauth2 = await getOath2();
|
|
||||||
reply.redirect(oauth2!.getAuthorizeUrl(params));
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get<{ Querystring: { code: string; state: string; } }>('/gh/cb', async (request, reply) => {
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
|
|
||||||
const oauth2 = await getOath2();
|
|
||||||
|
|
||||||
if (!userToken) {
|
|
||||||
const sessid = request.cookies['signin_with_github_sid'];
|
|
||||||
|
|
||||||
if (!sessid) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = request.query.code;
|
|
||||||
|
|
||||||
if (!code || typeof code !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(sessid, async (_, state) => {
|
|
||||||
if (state == null) throw new Error('empty state');
|
|
||||||
res(JSON.parse(state));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.query.state !== state) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
|
|
||||||
oauth2!.getOAuthAccessToken(code, {
|
|
||||||
redirect_uri,
|
|
||||||
}, (err, accessToken, refresh, result) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err);
|
|
||||||
} else if (result.error) {
|
|
||||||
rej(result.error);
|
|
||||||
} else {
|
|
||||||
res({ accessToken });
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
|
|
||||||
'Authorization': `bearer ${accessToken}`,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = await this.userProfilesRepository.createQueryBuilder()
|
|
||||||
.where('"integrations"->\'github\'->>\'id\' = :id', { id: id })
|
|
||||||
.andWhere('"userHost" IS NULL')
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (link == null) {
|
|
||||||
throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
|
||||||
} else {
|
|
||||||
const code = request.query.code;
|
|
||||||
|
|
||||||
if (!code || typeof code !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(userToken, async (_, state) => {
|
|
||||||
if (state == null) throw new Error('empty state');
|
|
||||||
res(JSON.parse(state));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.query.state !== state) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
|
|
||||||
oauth2!.getOAuthAccessToken(
|
|
||||||
code,
|
|
||||||
{ redirect_uri },
|
|
||||||
(err, accessToken, refresh, result) => {
|
|
||||||
if (err) {
|
|
||||||
rej(err);
|
|
||||||
} else if (result.error) {
|
|
||||||
rej(result.error);
|
|
||||||
} else {
|
|
||||||
res({ accessToken });
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
|
|
||||||
'Authorization': `bearer ${accessToken}`,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
|
|
||||||
if (typeof login !== 'string' || typeof id !== 'number') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: {
|
|
||||||
...profile.integrations,
|
|
||||||
github: {
|
|
||||||
accessToken: accessToken,
|
|
||||||
id: id,
|
|
||||||
login: login,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private getUserToken(request: FastifyRequest): string | null {
|
|
||||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private compareOrigin(request: FastifyRequest): boolean {
|
|
||||||
function normalizeUrl(url?: string): string {
|
|
||||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const referer = request.headers['referer'];
|
|
||||||
|
|
||||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
import * as autwh from 'autwh';
|
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
|
||||||
import type { ILocalUser } from '@/models/entities/User.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import { SigninService } from '../SigninService.js';
|
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TwitterServerService {
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.config)
|
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private httpRequestService: HttpRequestService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private metaService: MetaService,
|
|
||||||
private signinService: SigninService,
|
|
||||||
) {
|
|
||||||
//this.create = this.create.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
|
||||||
fastify.get('/disconnect/twitter', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (userToken == null) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
delete profile.integrations.twitter;
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: profile.integrations,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return 'Twitterの連携を解除しました :v:';
|
|
||||||
});
|
|
||||||
|
|
||||||
const getTwAuth = async () => {
|
|
||||||
const meta = await this.metaService.fetch(true);
|
|
||||||
|
|
||||||
if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
|
|
||||||
return autwh({
|
|
||||||
consumerKey: meta.twitterConsumerKey,
|
|
||||||
consumerSecret: meta.twitterConsumerSecret,
|
|
||||||
callbackUrl: `${this.config.url}/api/tw/cb`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fastify.get('/connect/twitter', async (request, reply) => {
|
|
||||||
if (!this.compareOrigin(request)) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid origin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
if (userToken == null) {
|
|
||||||
throw new FastifyReplyError(400, 'signin required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const twAuth = await getTwAuth();
|
|
||||||
const twCtx = await twAuth!.begin();
|
|
||||||
this.redisClient.set(userToken, JSON.stringify(twCtx));
|
|
||||||
reply.redirect(twCtx.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/signin/twitter', async (request, reply) => {
|
|
||||||
const twAuth = await getTwAuth();
|
|
||||||
const twCtx = await twAuth!.begin();
|
|
||||||
|
|
||||||
const sessid = uuid();
|
|
||||||
|
|
||||||
this.redisClient.set(sessid, JSON.stringify(twCtx));
|
|
||||||
|
|
||||||
reply.setCookie('signin_with_twitter_sid', sessid, {
|
|
||||||
path: '/',
|
|
||||||
secure: this.config.url.startsWith('https'),
|
|
||||||
httpOnly: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
reply.redirect(twCtx.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/tw/cb', async (request, reply) => {
|
|
||||||
const userToken = this.getUserToken(request);
|
|
||||||
|
|
||||||
const twAuth = await getTwAuth();
|
|
||||||
|
|
||||||
if (userToken == null) {
|
|
||||||
const sessid = request.cookies['signin_with_twitter_sid'];
|
|
||||||
|
|
||||||
if (sessid == null) {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(sessid, async (_, twCtx) => {
|
|
||||||
res(twCtx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const twCtx = await get;
|
|
||||||
|
|
||||||
const verifier = request.query.oauth_verifier;
|
|
||||||
if (!verifier || typeof verifier !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
|
||||||
|
|
||||||
const link = await this.userProfilesRepository.createQueryBuilder()
|
|
||||||
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
|
|
||||||
.andWhere('"userHost" IS NULL')
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (link == null) {
|
|
||||||
throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
|
|
||||||
} else {
|
|
||||||
const verifier = request.query.oauth_verifier;
|
|
||||||
|
|
||||||
if (!verifier || typeof verifier !== 'string') {
|
|
||||||
throw new FastifyReplyError(400, 'invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = new Promise<any>((res, rej) => {
|
|
||||||
this.redisClient.get(userToken, async (_, twCtx) => {
|
|
||||||
res(twCtx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const twCtx = await get;
|
|
||||||
|
|
||||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({
|
|
||||||
host: IsNull(),
|
|
||||||
token: userToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
|
||||||
|
|
||||||
await this.userProfilesRepository.update(user.id, {
|
|
||||||
integrations: {
|
|
||||||
...profile.integrations,
|
|
||||||
twitter: {
|
|
||||||
accessToken: result.accessToken,
|
|
||||||
accessTokenSecret: result.accessTokenSecret,
|
|
||||||
userId: result.userId,
|
|
||||||
screenName: result.screenName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish i updated event
|
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
|
|
||||||
detail: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private getUserToken(request: FastifyRequest): string | null {
|
|
||||||
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private compareOrigin(request: FastifyRequest): boolean {
|
|
||||||
function normalizeUrl(url?: string): string {
|
|
||||||
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const referer = request.headers['referer'];
|
|
||||||
|
|
||||||
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,11 +40,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="social">
|
|
||||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
|
|
||||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
|
|
||||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -164,11 +164,6 @@ const menuDef = $computed(() => [{
|
||||||
text: i18n.ts.relays,
|
text: i18n.ts.relays,
|
||||||
to: '/admin/relays',
|
to: '/admin/relays',
|
||||||
active: currentPage?.route.name === 'relays',
|
active: currentPage?.route.name === 'relays',
|
||||||
}, {
|
|
||||||
icon: 'ti ti-share',
|
|
||||||
text: i18n.ts.integration,
|
|
||||||
to: '/admin/integrations',
|
|
||||||
active: currentPage?.route.name === 'integrations',
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-ban',
|
icon: 'ti ti-ban',
|
||||||
text: i18n.ts.instanceBlocking,
|
text: i18n.ts.instanceBlocking,
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
<template>
|
|
||||||
<FormSuspense :p="init">
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<MkSwitch v-model="enableDiscordIntegration">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
|
|
||||||
<template v-if="enableDiscordIntegration">
|
|
||||||
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
|
|
||||||
|
|
||||||
<MkInput v-model="discordClientId">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Client ID</template>
|
|
||||||
</MkInput>
|
|
||||||
|
|
||||||
<MkInput v-model="discordClientSecret">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Client Secret</template>
|
|
||||||
</MkInput>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</FormSuspense>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { } from 'vue';
|
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
|
||||||
import MkInput from '@/components/MkInput.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { fetchInstance } from '@/instance';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
|
|
||||||
let uri: string = $ref('');
|
|
||||||
let enableDiscordIntegration: boolean = $ref(false);
|
|
||||||
let discordClientId: string | null = $ref(null);
|
|
||||||
let discordClientSecret: string | null = $ref(null);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const meta = await os.api('admin/meta');
|
|
||||||
uri = meta.uri;
|
|
||||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
|
||||||
discordClientId = meta.discordClientId;
|
|
||||||
discordClientSecret = meta.discordClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
os.apiWithDialog('admin/update-meta', {
|
|
||||||
enableDiscordIntegration,
|
|
||||||
discordClientId,
|
|
||||||
discordClientSecret,
|
|
||||||
}).then(() => {
|
|
||||||
fetchInstance();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,60 +0,0 @@
|
||||||
<template>
|
|
||||||
<FormSuspense :p="init">
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<MkSwitch v-model="enableGithubIntegration">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
|
|
||||||
<template v-if="enableGithubIntegration">
|
|
||||||
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
|
|
||||||
|
|
||||||
<MkInput v-model="githubClientId">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Client ID</template>
|
|
||||||
</MkInput>
|
|
||||||
|
|
||||||
<MkInput v-model="githubClientSecret">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Client Secret</template>
|
|
||||||
</MkInput>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</FormSuspense>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { } from 'vue';
|
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
|
||||||
import MkInput from '@/components/MkInput.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { fetchInstance } from '@/instance';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
|
|
||||||
let uri: string = $ref('');
|
|
||||||
let enableGithubIntegration: boolean = $ref(false);
|
|
||||||
let githubClientId: string | null = $ref(null);
|
|
||||||
let githubClientSecret: string | null = $ref(null);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const meta = await os.api('admin/meta');
|
|
||||||
uri = meta.uri;
|
|
||||||
enableGithubIntegration = meta.enableGithubIntegration;
|
|
||||||
githubClientId = meta.githubClientId;
|
|
||||||
githubClientSecret = meta.githubClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
os.apiWithDialog('admin/update-meta', {
|
|
||||||
enableGithubIntegration,
|
|
||||||
githubClientId,
|
|
||||||
githubClientSecret,
|
|
||||||
}).then(() => {
|
|
||||||
fetchInstance();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,60 +0,0 @@
|
||||||
<template>
|
|
||||||
<FormSuspense :p="init">
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<MkSwitch v-model="enableTwitterIntegration">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
|
|
||||||
<template v-if="enableTwitterIntegration">
|
|
||||||
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
|
|
||||||
|
|
||||||
<MkInput v-model="twitterConsumerKey">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Consumer Key</template>
|
|
||||||
</MkInput>
|
|
||||||
|
|
||||||
<MkInput v-model="twitterConsumerSecret">
|
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
|
||||||
<template #label>Consumer Secret</template>
|
|
||||||
</MkInput>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</FormSuspense>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
|
||||||
import MkInput from '@/components/MkInput.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { fetchInstance } from '@/instance';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
|
|
||||||
let uri: string = $ref('');
|
|
||||||
let enableTwitterIntegration: boolean = $ref(false);
|
|
||||||
let twitterConsumerKey: string | null = $ref(null);
|
|
||||||
let twitterConsumerSecret: string | null = $ref(null);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const meta = await os.api('admin/meta');
|
|
||||||
uri = meta.uri;
|
|
||||||
enableTwitterIntegration = meta.enableTwitterIntegration;
|
|
||||||
twitterConsumerKey = meta.twitterConsumerKey;
|
|
||||||
twitterConsumerSecret = meta.twitterConsumerSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
os.apiWithDialog('admin/update-meta', {
|
|
||||||
enableTwitterIntegration,
|
|
||||||
twitterConsumerKey,
|
|
||||||
twitterConsumerSecret,
|
|
||||||
}).then(() => {
|
|
||||||
fetchInstance();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,61 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkStickyContainer>
|
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
|
||||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
|
||||||
<FormSuspense :p="init">
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<MkFolder>
|
|
||||||
<template #icon><i class="ti ti-brand-twitter"></i></template>
|
|
||||||
<template #label>Twitter</template>
|
|
||||||
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
|
||||||
<XTwitter/>
|
|
||||||
</MkFolder>
|
|
||||||
<MkFolder>
|
|
||||||
<template #icon><i class="ti ti-brand-github"></i></template>
|
|
||||||
<template #label>GitHub</template>
|
|
||||||
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
|
||||||
<XGithub/>
|
|
||||||
</MkFolder>
|
|
||||||
<MkFolder>
|
|
||||||
<template #icon><i class="ti ti-brand-discord"></i></template>
|
|
||||||
<template #label>Discord</template>
|
|
||||||
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
|
||||||
<XDiscord/>
|
|
||||||
</MkFolder>
|
|
||||||
</div>
|
|
||||||
</FormSuspense>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { } from 'vue';
|
|
||||||
import XTwitter from './integrations.twitter.vue';
|
|
||||||
import XGithub from './integrations.github.vue';
|
|
||||||
import XDiscord from './integrations.discord.vue';
|
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
||||||
|
|
||||||
let enableTwitterIntegration: boolean = $ref(false);
|
|
||||||
let enableGithubIntegration: boolean = $ref(false);
|
|
||||||
let enableDiscordIntegration: boolean = $ref(false);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const meta = await os.api('admin/meta');
|
|
||||||
enableTwitterIntegration = meta.enableTwitterIntegration;
|
|
||||||
enableGithubIntegration = meta.enableGithubIntegration;
|
|
||||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
|
||||||
title: i18n.ts.integration,
|
|
||||||
icon: 'ti ti-share',
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -89,11 +89,6 @@ const menuDef = computed(() => [{
|
||||||
text: i18n.ts.email,
|
text: i18n.ts.email,
|
||||||
to: '/settings/email',
|
to: '/settings/email',
|
||||||
active: currentPage?.route.name === 'email',
|
active: currentPage?.route.name === 'email',
|
||||||
}, {
|
|
||||||
icon: 'ti ti-share',
|
|
||||||
text: i18n.ts.integration,
|
|
||||||
to: '/settings/integration',
|
|
||||||
active: currentPage?.route.name === 'integration',
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-lock',
|
icon: 'ti ti-lock',
|
||||||
text: i18n.ts.security,
|
text: i18n.ts.security,
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<FormSection v-if="instance.enableTwitterIntegration">
|
|
||||||
<template #label><i class="ti ti-brand-twitter"></i> Twitter</template>
|
|
||||||
<p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
|
|
||||||
<MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton>
|
|
||||||
<MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton>
|
|
||||||
</FormSection>
|
|
||||||
|
|
||||||
<FormSection v-if="instance.enableDiscordIntegration">
|
|
||||||
<template #label><i class="ti ti-brand-discord"></i> Discord</template>
|
|
||||||
<p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
|
|
||||||
<MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton>
|
|
||||||
<MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton>
|
|
||||||
</FormSection>
|
|
||||||
|
|
||||||
<FormSection v-if="instance.enableGithubIntegration">
|
|
||||||
<template #label><i class="ti ti-brand-github"></i> GitHub</template>
|
|
||||||
<p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
|
|
||||||
<MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton>
|
|
||||||
<MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton>
|
|
||||||
</FormSection>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
import { apiUrl } from '@/config';
|
|
||||||
import FormSection from '@/components/form/section.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
import { instance } from '@/instance';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
||||||
|
|
||||||
const twitterForm = ref<Window | null>(null);
|
|
||||||
const discordForm = ref<Window | null>(null);
|
|
||||||
const githubForm = ref<Window | null>(null);
|
|
||||||
|
|
||||||
const integrations = computed(() => $i!.integrations);
|
|
||||||
|
|
||||||
function openWindow(service: string, type: string) {
|
|
||||||
return window.open(`${apiUrl}/${type}/${service}`,
|
|
||||||
`${service}_${type}_window`,
|
|
||||||
'height=570, width=520',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectTwitter() {
|
|
||||||
twitterForm.value = openWindow('twitter', 'connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectTwitter() {
|
|
||||||
openWindow('twitter', 'disconnect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectDiscord() {
|
|
||||||
discordForm.value = openWindow('discord', 'connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectDiscord() {
|
|
||||||
openWindow('discord', 'disconnect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectGithub() {
|
|
||||||
githubForm.value = openWindow('github', 'connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectGithub() {
|
|
||||||
openWindow('github', 'disconnect');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.cookie = `igi=${$i!.token}; path=/;` +
|
|
||||||
' max-age=31536000;' +
|
|
||||||
(document.location.protocol.startsWith('https') ? ' secure' : '');
|
|
||||||
|
|
||||||
watch(integrations, () => {
|
|
||||||
if (integrations.value.twitter) {
|
|
||||||
if (twitterForm.value) twitterForm.value.close();
|
|
||||||
}
|
|
||||||
if (integrations.value.discord) {
|
|
||||||
if (discordForm.value) discordForm.value.close();
|
|
||||||
}
|
|
||||||
if (integrations.value.github) {
|
|
||||||
if (githubForm.value) githubForm.value.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
|
||||||
title: i18n.ts.integration,
|
|
||||||
icon: 'ti ti-share',
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -70,10 +70,6 @@ export const routes = [{
|
||||||
path: '/email',
|
path: '/email',
|
||||||
name: 'email',
|
name: 'email',
|
||||||
component: page(() => import('./pages/settings/email.vue')),
|
component: page(() => import('./pages/settings/email.vue')),
|
||||||
}, {
|
|
||||||
path: '/integration',
|
|
||||||
name: 'integration',
|
|
||||||
component: page(() => import('./pages/settings/integration.vue')),
|
|
||||||
}, {
|
}, {
|
||||||
path: '/security',
|
path: '/security',
|
||||||
name: 'security',
|
name: 'security',
|
||||||
|
@ -399,10 +395,6 @@ export const routes = [{
|
||||||
path: '/relays',
|
path: '/relays',
|
||||||
name: 'relays',
|
name: 'relays',
|
||||||
component: page(() => import('./pages/admin/relays.vue')),
|
component: page(() => import('./pages/admin/relays.vue')),
|
||||||
}, {
|
|
||||||
path: '/integrations',
|
|
||||||
name: 'integrations',
|
|
||||||
component: page(() => import('./pages/admin/integrations.vue')),
|
|
||||||
}, {
|
}, {
|
||||||
path: '/instance-block',
|
path: '/instance-block',
|
||||||
name: 'instance-block',
|
name: 'instance-block',
|
||||||
|
|
Loading…
Reference in a new issue