From 90f738608f66a00b92e6c5afa86264317d0c480d Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Feb 2021 16:06:19 +0900 Subject: アカウント情報が更新されたときに永続化されない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/account.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/client/account.ts b/src/client/account.ts index e6ee8613d2..05faeb3220 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -62,6 +62,7 @@ export function updateAccount(data) { for (const [key, value] of Object.entries(data)) { $i[key] = value; } + localStorage.setItem('account', JSON.stringify($i)); } export function refreshAccount() { -- cgit v1.2.3-freya From cc9bc7703a1d5c56f2bb10dc87a1724b674e6413 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Feb 2021 16:16:04 +0900 Subject: スクラッチパッド、ボタンウィジェット、AiScriptウィジェットでトークンを設定するように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/pages/scratchpad.vue | 3 ++- src/client/widgets/aiscript.vue | 3 ++- src/client/widgets/button.vue | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue index e070f477be..6ee7ec02fc 100644 --- a/src/client/pages/scratchpad.vue +++ b/src/client/pages/scratchpad.vue @@ -70,7 +70,8 @@ export default defineComponent({ async run() { this.logs = []; const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'scratchpad' + storageKey: 'scratchpad', + token: this.$i?.token, }), { in: (q) => { return new Promise(ok => { diff --git a/src/client/widgets/aiscript.vue b/src/client/widgets/aiscript.vue index 4e788b4b4a..f885db886b 100644 --- a/src/client/widgets/aiscript.vue +++ b/src/client/widgets/aiscript.vue @@ -54,7 +54,8 @@ export default defineComponent({ async run() { this.logs = []; const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'widget' + storageKey: 'widget', + token: this.$i?.token, }), { in: (q) => { return new Promise(ok => { diff --git a/src/client/widgets/button.vue b/src/client/widgets/button.vue index db247d36b5..3417181d0c 100644 --- a/src/client/widgets/button.vue +++ b/src/client/widgets/button.vue @@ -45,7 +45,8 @@ export default defineComponent({ methods: { async run() { const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'scratchpad' + storageKey: 'widget', + token: this.$i?.token, }), { in: (q) => { return new Promise(ok => { -- cgit v1.2.3-freya From 263032b6806ba3d296ba2956716de34c7d42ebe3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Feb 2021 17:47:30 +0900 Subject: HTMLメール MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/mi-white.png | Bin 0 -> 18767 bytes src/services/send-email.ts | 84 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 assets/mi-white.png (limited to 'src') diff --git a/assets/mi-white.png b/assets/mi-white.png new file mode 100644 index 0000000000..1e57da6b38 Binary files /dev/null and b/assets/mi-white.png differ diff --git a/src/services/send-email.ts b/src/services/send-email.ts index 5a8f92be54..e713ea78d5 100644 --- a/src/services/send-email.ts +++ b/src/services/send-email.ts @@ -8,6 +8,9 @@ export const logger = new Logger('email'); export async function sendEmail(to: string, subject: string, text: string) { const meta = await fetchMeta(true); + const iconUrl = `${config.url}/assets/mi-white.png`; + const emailSettingUrl = `${config.url}/settings/email`; + const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; const transporter = nodemailer.createTransport({ @@ -26,8 +29,85 @@ export async function sendEmail(to: string, subject: string, text: string) { const info = await transporter.sendMail({ from: meta.email!, to: to, - subject: subject || 'Misskey', - text: text + subject: subject, + text: text, + html: ` + + + + ${ subject } + + + +
+
+ +
+
+

${ subject }

+
${ text }
+
+ +
+ + + + ` }); logger.info('Message sent: %s', info.messageId); -- cgit v1.2.3-freya From ab9343a788aa7c2a3f51a2adcb26780a56906af2 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Feb 2021 17:49:28 +0900 Subject: add note --- src/services/send-email.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/services/send-email.ts b/src/services/send-email.ts index e713ea78d5..151228c7e3 100644 --- a/src/services/send-email.ts +++ b/src/services/send-email.ts @@ -26,6 +26,7 @@ export async function sendEmail(to: string, subject: string, text: string) { } as any); try { + // TODO: htmlサニタイズ const info = await transporter.sendMail({ from: meta.email!, to: to, -- cgit v1.2.3-freya From 33bd8fb9d97408a778c38a846e2e44a31fd95c38 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Feb 2021 22:56:58 +0900 Subject: fix ui --- src/client/pages/settings/plugin.install.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/pages/settings/plugin.install.vue b/src/client/pages/settings/plugin.install.vue index 34c62619ad..0873f28d23 100644 --- a/src/client/pages/settings/plugin.install.vue +++ b/src/client/pages/settings/plugin.install.vue @@ -1,6 +1,6 @@ @@ -50,10 +53,10 @@ import FormButton from '@/components/form/button.vue'; import FormInput from '@/components/form/input.vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormSwitch from '@/components/form/switch.vue'; -import FormTuple from '@/components/form/tuple.vue'; +import FormSelect from '@/components/form/select.vue'; import FormBase from '@/components/form/base.vue'; import FormGroup from '@/components/form/group.vue'; -import { host } from '@/config'; +import { host, langs } from '@/config'; import { selectFile } from '@/scripts/select-file'; import * as os from '@/os'; @@ -63,7 +66,7 @@ export default defineComponent({ FormInput, FormTextarea, FormSwitch, - FormTuple, + FormSelect, FormBase, FormGroup, }, @@ -77,9 +80,11 @@ export default defineComponent({ icon: faUser }, host, + langs, name: null, description: null, birthday: null, + lang: null, location: null, fieldName0: null, fieldValue0: null, @@ -104,6 +109,7 @@ export default defineComponent({ this.description = this.$i.description; this.location = this.$i.location; this.birthday = this.$i.birthday; + this.lang = this.$i.lang; this.avatarId = this.$i.avatarId; this.bannerId = this.$i.bannerId; this.isBot = this.$i.isBot; @@ -118,6 +124,15 @@ export default defineComponent({ this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null; this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null; this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null; + + this.$watch('name', this.save); + this.$watch('description', this.save); + this.$watch('location', this.save); + this.$watch('birthday', this.save); + this.$watch('lang', this.save); + this.$watch('isBot', this.save); + this.$watch('isCat', this.save); + this.$watch('alwaysMarkNsfw', this.save); }, mounted() { @@ -214,14 +229,15 @@ export default defineComponent({ }); }, - save(notify) { + save() { this.saving = true; - os.api('i/update', { + os.apiWithDialog('i/update', { name: this.name || null, description: this.description || null, location: this.location || null, birthday: this.birthday || null, + lang: this.lang || null, isBot: !!this.isBot, isCat: !!this.isCat, alwaysMarkNsfw: !!this.alwaysMarkNsfw, @@ -231,16 +247,8 @@ export default defineComponent({ this.$i.avatarUrl = i.avatarUrl; this.$i.bannerId = i.bannerId; this.$i.bannerUrl = i.bannerUrl; - - if (notify) { - os.success(); - } }).catch(err => { this.saving = false; - os.dialog({ - type: 'error', - text: err.id - }); }); }, } diff --git a/src/client/scripts/i18n.ts b/src/client/scripts/i18n.ts deleted file mode 100644 index d535e236bb..0000000000 --- a/src/client/scripts/i18n.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Notice: Service Workerでも使用します -export class I18n> { - public locale: T; - - constructor(locale: T) { - this.locale = locale; - - if (_DEV_) { - console.log('i18n', this.locale); - } - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record): string { - try { - let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; - - if (_DEV_) { - if (!str.includes('{')) { - console.warn(`i18n: '${key}' has no any arg. so ref prop directly instead of call this method.`); - } - } - - if (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v); - } - } - return str; - } catch (e) { - if (_DEV_) { - console.warn(`missing localization '${key}'`); - return `⚠'${key}'⚠`; - } - - return key; - } - } -} diff --git a/src/client/sw/sw.ts b/src/client/sw/sw.ts index a18d305ea1..c93fe4926d 100644 --- a/src/client/sw/sw.ts +++ b/src/client/sw/sw.ts @@ -5,7 +5,7 @@ declare var self: ServiceWorkerGlobalScope; import { get, set } from 'idb-keyval'; import composeNotification from '@/sw/compose-notification'; -import { I18n } from '@/scripts/i18n'; +import { I18n } from '../../misc/i18n'; //#region Variables const version = _VERSION_; diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts new file mode 100644 index 0000000000..4fa398763a --- /dev/null +++ b/src/misc/i18n.ts @@ -0,0 +1,29 @@ +export class I18n> { + public locale: T; + + constructor(locale: T) { + this.locale = locale; + + //#region BIND + this.t = this.t.bind(this); + //#endregion + } + + // string にしているのは、ドット区切りでのパス指定を許可するため + // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも + public t(key: string, args?: Record): string { + try { + let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; + + if (args) { + for (const [k, v] of Object.entries(args)) { + str = str.replace(`{${k}}`, v); + } + } + return str; + } catch (e) { + console.warn(`missing localization '${key}'`); + return key; + } + } +} diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts index 4fab52868f..3a9043fac6 100644 --- a/src/models/entities/user-profile.ts +++ b/src/models/entities/user-profile.ts @@ -4,6 +4,8 @@ import { User } from './user'; import { Page } from './page'; import { notificationTypes } from '../../types'; +// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも +// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン @Entity() export class UserProfile { @PrimaryColumn(id()) @@ -41,6 +43,11 @@ export class UserProfile { value: string; }[]; + @Column('varchar', { + length: 32, nullable: true, + }) + public lang: string | null; + @Column('varchar', { length: 512, nullable: true, comment: 'Remote URL of the user.' @@ -63,6 +70,11 @@ export class UserProfile { }) public emailVerified: boolean; + @Column('jsonb', { + default: ['follow', 'receiveFollowRequest', 'groupInvited'] + }) + public emailNotificationTypes: string[]; + @Column('varchar', { length: 128, nullable: true, }) diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 88861224a4..a3453b1aa6 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -213,6 +213,7 @@ export class UserRepository extends Repository { description: profile!.description, location: profile!.location, birthday: profile!.birthday, + lang: profile!.lang, fields: profile!.fields, followersCount: user.followersCount, followingCount: user.followingCount, @@ -258,7 +259,8 @@ export class UserRepository extends Repository { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), integrations: profile!.integrations, mutedWords: profile!.mutedWords, - mutingNotificationTypes: profile?.mutingNotificationTypes, + mutingNotificationTypes: profile!.mutingNotificationTypes, + emailNotificationTypes: profile!.emailNotificationTypes, } : {}), ...(opts.includeSecrets ? { diff --git a/src/server/api/endpoints/admin/send-email.ts b/src/server/api/endpoints/admin/send-email.ts index 9af931ad99..c0e77e1621 100644 --- a/src/server/api/endpoints/admin/send-email.ts +++ b/src/server/api/endpoints/admin/send-email.ts @@ -22,5 +22,5 @@ export const meta = { }; export default define(meta, async (ps) => { - await sendEmail(ps.to, ps.subject, ps.text); + await sendEmail(ps.to, ps.subject, ps.text, ps.text); }); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts index 20d9703320..d3d7bace75 100644 --- a/src/server/api/endpoints/i/update-email.ts +++ b/src/server/api/endpoints/i/update-email.ts @@ -72,7 +72,9 @@ export default define(meta, async (ps, user) => { const link = `${config.url}/verify-email/${code}`; - sendEmail(ps.email, 'Email verification', `To verify email, please click this link: ${link}`); + sendEmail(ps.email, 'Email verification', + `To verify email, please click this link:
${link}`, + `To verify email, please click this link: ${link}`); } return iObj; diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index e4c0e8cec9..bf1796924a 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -161,6 +161,10 @@ export const meta = { mutingNotificationTypes: { validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])) }, + + emailNotificationTypes: { + validator: $.optional.arr($.str) + }, }, errors: { @@ -206,7 +210,7 @@ export default define(meta, async (ps, user, token) => { if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) profileUpdates.description = ps.description; - //if (ps.lang !== undefined) updates.lang = ps.lang; + 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.avatarId !== undefined) updates.avatarId = ps.avatarId; @@ -226,6 +230,7 @@ export default define(meta, async (ps, user, token) => { 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 (ps.avatarId) { const avatar = await DriveFiles.findOne(ps.avatarId); diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index 5dddaa5727..6cd116040a 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -4,6 +4,7 @@ import { Notifications, Mutings, UserProfiles } from '../models'; import { genId } from '../misc/gen-id'; import { User } from '../models/entities/user'; import { Notification } from '../models/entities/notification'; +import { sendEmailNotification } from './send-email-notification'; export async function createNotification( notifieeId: User['id'], @@ -38,20 +39,22 @@ export async function createNotification( setTimeout(async () => { const fresh = await Notifications.findOne(notification.id); if (fresh == null) return; // 既に削除されているかもしれない - if (!fresh.isRead) { - //#region ただしミュートしているユーザーからの通知なら無視 - const mutings = await Mutings.find({ - muterId: notifieeId - }); - if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { - return; - } - //#endregion - - publishMainStream(notifieeId, 'unreadNotification', packed); - - pushSw(notifieeId, 'notification', packed); + if (fresh.isRead) return; + + //#region ただしミュートしているユーザーからの通知なら無視 + const mutings = await Mutings.find({ + muterId: notifieeId + }); + if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { + return; } + //#endregion + + publishMainStream(notifieeId, 'unreadNotification', packed); + + pushSw(notifieeId, 'notification', packed); + if (type === 'follow') sendEmailNotification.follow(notifieeId, data); + if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, data); }, 2000); return notification; diff --git a/src/services/send-email-notification.ts b/src/services/send-email-notification.ts new file mode 100644 index 0000000000..7579d5b674 --- /dev/null +++ b/src/services/send-email-notification.ts @@ -0,0 +1,28 @@ +import { UserProfiles } from '../models'; +import { User } from '../models/entities/user'; +import { sendEmail } from './send-email'; +import * as locales from '../../locales/'; +import { I18n } from '../misc/i18n'; + +// TODO: locale ファイルをクライアント用とサーバー用で分けたい + +async function follow(userId: User['id'], args: {}) { + const userProfile = await UserProfiles.findOneOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; + const locale = locales[userProfile.lang || 'ja-JP']; + const i18n = new I18n(locale); + sendEmail(userProfile.email, i18n.t('_email._follow.title'), 'test', 'test'); +} + +async function receiveFollowRequest(userId: User['id'], args: {}) { + const userProfile = await UserProfiles.findOneOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; + const locale = locales[userProfile.lang || 'ja-JP']; + const i18n = new I18n(locale); + sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), 'test', 'test'); +} + +export const sendEmailNotification = { + follow, + receiveFollowRequest, +}; diff --git a/src/services/send-email.ts b/src/services/send-email.ts index 151228c7e3..c716b36715 100644 --- a/src/services/send-email.ts +++ b/src/services/send-email.ts @@ -5,7 +5,7 @@ import config from '../config'; export const logger = new Logger('email'); -export async function sendEmail(to: string, subject: string, text: string) { +export async function sendEmail(to: string, subject: string, html: string, text: string) { const meta = await fetchMeta(true); const iconUrl = `${config.url}/assets/mi-white.png`; @@ -44,6 +44,9 @@ export async function sendEmail(to: string, subject: string, text: string) { body { padding: 16px; + margin: 0; + font-family: sans-serif; + font-size: 14px; } a { @@ -67,6 +70,7 @@ export async function sendEmail(to: string, subject: string, text: string) { main > header > img { max-width: 128px; max-height: 28px; + vertical-align: bottom; } main > article { padding: 32px; @@ -97,7 +101,7 @@ export async function sendEmail(to: string, subject: string, text: string) {

${ subject }

-
${ text }
+
${ html }