koishi-plugin-chat-patch 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  <link rel="stylesheet" href="./bcui/css/style.css">
12
12
  <link rel="stylesheet" href="./bcui/css/color-light.css">
13
13
  <link rel="stylesheet" href="./css/append-light.css">
14
- <script type="module" crossorigin src="./assets/index--7-eCx0r.js"></script>
14
+ <script type="module" crossorigin src="./assets/index-DOJoilEy.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="./assets/index-X9hFOAza.css">
16
16
  </head>
17
17
 
@@ -1047,9 +1047,6 @@ msgstr ""
1047
1047
  msgid "应用内通知和系统通知"
1048
1048
  msgstr ""
1049
1049
 
1050
- msgid "成员设置"
1051
- msgstr ""
1052
-
1053
1050
  msgid "成员信息"
1054
1051
  msgstr ""
1055
1052
 
@@ -103,17 +103,17 @@
103
103
  class="msg-md" />
104
104
  <img v-else-if="item.type == 'image' && item.file == 'marketface'"
105
105
  :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
106
- :src="item.url"
106
+ :src="getImageUrl(item)"
107
107
  :alt="item.summary"
108
108
  @load="imageLoaded"
109
- @click="preImgClick(item.url || item.file)"
109
+ @click="preImgClick(getImageUrl(item))"
110
110
  @error="imgLoadFail">
111
111
  <img v-else-if="item.type == 'mface'"
112
112
  :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
113
- :src="item.url"
113
+ :src="getImageUrl(item)"
114
114
  :alt="item.summary"
115
115
  @load="imageLoaded"
116
- @click="preImgClick(item.url || item.file)"
116
+ @click="preImgClick(getImageUrl(item))"
117
117
  @error="imgLoadFail">
118
118
  <template v-else-if="item.type == 'image'">
119
119
  <div v-show="shouldShowImagePlaceholder(item, Number(index))"
@@ -121,21 +121,21 @@
121
121
  :class="imgStyle(data.message.length, Number(index), isFace(item)) + ' msg-img-placeholder'"
122
122
  @click="loadImage(item, Number(index), $event)">
123
123
  <font-awesome-icon
124
- :icon="['fas', imageLoading(getImageKey(Number(index), item.url)) ? 'spinner' : 'image']"
125
- :spin="imageLoading(getImageKey(Number(index), item.url))" />
124
+ :icon="['fas', imageLoading(getImageKey(Number(index), getImageUrl(item))) ? 'spinner' : 'image']"
125
+ :spin="imageLoading(getImageKey(Number(index), getImageUrl(item)))" />
126
126
  <span>
127
- {{ imageLoading(getImageKey(Number(index), item.url)) ? $t('加载中') : $t('点击加载图片') }}
127
+ {{ imageLoading(getImageKey(Number(index), getImageUrl(item))) ? $t('加载中') : $t('点击加载图片') }}
128
128
  </span>
129
129
  </div>
130
130
  <img v-show="!shouldShowImagePlaceholder(item, Number(index))"
131
131
  :title="(!item.summary || item.summary == '') ? $t('预览图片') : item.summary"
132
132
  :alt="$t('图片')"
133
133
  :class=" imgStyle(data.message.length, Number(index), isFace(item))"
134
- :src="getImgSrc(item.url)"
134
+ :src="getImgSrc(getImageUrl(item))"
135
135
  data-type="image"
136
136
  @load="imageLoaded"
137
137
  @error="imgLoadFail"
138
- @click="imgClick(item.url || item.file, $event)">
138
+ @click="imgClick(getImageUrl(item), $event)">
139
139
  </template>
140
140
  <template v-else-if="item.type == 'face'">
141
141
  <EmojiFace :emoji="Emoji.get(Number(item.id))" class="msg-face" />
@@ -173,7 +173,8 @@
173
173
  <div v-if="data.fileView && Object.keys(data.fileView).length > 0"
174
174
  class="file-view">
175
175
  <img v-if="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(data.fileView.ext)"
176
- :src="getMediaSrc(data.fileView.url)">
176
+ :src="getMediaSrc(data.fileView.url)"
177
+ @click="preImgClick(getMediaSrc(data.fileView.url))">
177
178
  <video v-else-if="['mp4', 'avi', 'mkv', 'flv'].includes(data.fileView.ext)"
178
179
  playsinline controls muted
179
180
  autoplay>
@@ -570,11 +571,16 @@ async function loadCachedImages() {
570
571
  const selfId = authStore.loginInfo?.uin
571
572
  if (!selfId) return
572
573
  for (const seg of data.message) {
573
- if (seg.type !== 'image' || !seg.url) continue
574
- await loadCachedImage(seg.url)
574
+ const url = getImageUrl(seg)
575
+ if (seg.type !== 'image' || !url) continue
576
+ await loadCachedImage(url)
575
577
  }
576
578
  }
577
579
 
580
+ function getImageUrl(item: { url?: string; file?: string }): string {
581
+ return String(item?.url || item?.file || '')
582
+ }
583
+
578
584
  async function loadCachedImage(url: string) {
579
585
  if (resolvedImages.value[url]) return resolvedImages.value[url]
580
586
 
@@ -593,6 +599,7 @@ async function loadCachedImage(url: string) {
593
599
  }
594
600
 
595
601
  function getImgSrc(url: string): string {
602
+ if (!url) return ''
596
603
  if (url.startsWith('base64://')) {
597
604
  return `data:image/png;base64,${url.slice(9)}`
598
605
  }
@@ -617,11 +624,11 @@ function imageLoading(key: string) {
617
624
  return pendingImageLoads.value[key] === true
618
625
  }
619
626
 
620
- function shouldShowImagePlaceholder(item: { type: string, url: string }, index: number) {
627
+ function shouldShowImagePlaceholder(item: { type: string, url?: string, file?: string }, index: number) {
621
628
  if (item.type !== 'image') return false
622
629
  if (settingsStore.sysConfig.opt_no_auto_load_image !== true) return false
623
630
 
624
- return manualImageLoads.value[getImageKey(index, item.url)] !== true
631
+ return manualImageLoads.value[getImageKey(index, getImageUrl(item))] !== true
625
632
  }
626
633
 
627
634
  function getAtClass(who: number | string) {
@@ -717,8 +724,9 @@ function buildMessageImageList(): Img | undefined {
717
724
  const collect = (msg: any) => {
718
725
  if (!Array.isArray(msg?.message)) return
719
726
  for (const item of msg.message) {
720
- if (item?.type === 'image' && item?.file !== 'marketface' && item?.url) {
721
- urls.push(String(item.url))
727
+ if (item?.type === 'image' && item?.file !== 'marketface') {
728
+ const url = getImageUrl(item)
729
+ if (url) urls.push(url)
722
730
  }
723
731
  }
724
732
  }
@@ -729,15 +737,16 @@ function buildMessageImageList(): Img | undefined {
729
737
  return Img.fromList([...new Set(urls)])
730
738
  }
731
739
 
732
- async function loadImage(item: { url: string }, index: number, _event?: MouseEvent) {
740
+ async function loadImage(item: { url?: string; file?: string }, index: number, _event?: MouseEvent) {
733
741
  if (props.selecting) return
734
- const key = getImageKey(index, item.url)
742
+ const url = getImageUrl(item)
743
+ const key = getImageKey(index, url)
735
744
  if (manualImageLoads.value[key] || pendingImageLoads.value[key]) return
736
745
 
737
746
  pendingImageLoads.value[key] = true
738
747
  try {
739
748
  if (data._from_local_db) {
740
- await loadCachedImage(item.url)
749
+ await loadCachedImage(url)
741
750
  }
742
751
  manualImageLoads.value[key] = true
743
752
  } finally {
@@ -1252,7 +1261,7 @@ async function openMerge(event?: MouseEvent) {
1252
1261
  imgList.push({
1253
1262
  index: index,
1254
1263
  message_id: item.message_id,
1255
- img_url: msg.url,
1264
+ img_url: msg.url || msg.file,
1256
1265
  })
1257
1266
  index++
1258
1267
  }
@@ -1303,6 +1303,8 @@ export interface SaveSelfMessagePayload {
1303
1303
  forwardId?: string
1304
1304
  forwardContent?: unknown[]
1305
1305
  sentAt?: number
1306
+ timestamp?: number
1307
+ timestampMs?: number
1306
1308
  source?: 'webui' | 'bot' | 'plugin'
1307
1309
  kind?: string
1308
1310
  }
@@ -1330,6 +1332,8 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1330
1332
  const selfId = getString(obj.selfId)
1331
1333
  const channelId = getString(obj.channelId)
1332
1334
  const sentAt = Number(obj.sentAt ?? Date.now())
1335
+ const timestamp = Number(obj.timestamp) || Math.floor(sentAt / 1000)
1336
+ const timestampMs = Number(obj.timestampMs ?? sentAt)
1333
1337
  const messageId = getString(obj.messageId)
1334
1338
  const localId = getString(obj.id) || `self-${sentAt}`
1335
1339
  // 只按记录里的显式 channelType 判断,不根据频道 ID 前缀推断
@@ -1399,10 +1403,11 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1399
1403
  channel_id: channelId,
1400
1404
  guild_id: guildId || undefined,
1401
1405
  target_id: selfId,
1402
- time: Math.floor(sentAt / 1000) || Math.floor(Date.now() / 1000),
1403
- local_time: sentAt,
1404
- timestamp_ms: sentAt,
1405
- time_ms: sentAt,
1406
+ timestamp,
1407
+ time: timestamp,
1408
+ local_time: timestampMs,
1409
+ timestamp_ms: timestampMs,
1410
+ time_ms: timestampMs,
1406
1411
  sender: {
1407
1412
  user_id: selfId,
1408
1413
  nickname: getString(login?.name) || selfId,
@@ -1469,8 +1474,9 @@ export async function loadChatHistoryFromCache(params: {
1469
1474
  const raw = getObject(recordObj.raw)
1470
1475
  const msg = satoriEventToOneBot(raw, params.platform)
1471
1476
  if (!msg || !msg.message_id) continue
1472
- const localTime = Number(recordObj.receivedAt ?? recordObj.timestampMs ?? recordObj.timestamp ?? 0)
1473
- if (localTime) {
1477
+ const rawTime = Number(recordObj.timestampMs ?? recordObj.timestamp ?? recordObj.receivedAt ?? 0)
1478
+ const localTime = rawTime > 1e12 ? rawTime : rawTime * 1000
1479
+ if (localTime > 0) {
1474
1480
  msg.local_time = localTime
1475
1481
  msg.timestamp_ms = localTime
1476
1482
  msg.time_ms = localTime
@@ -1039,6 +1039,8 @@ const msgFunctions = {
1039
1039
  message: Array.isArray(sentItem?.message) ? sentItem.message : [],
1040
1040
  source: 'webui',
1041
1041
  sentAt: Number(sentItem?.local_time ?? sentItem?.timestamp_ms ?? Date.now()),
1042
+ timestamp: Number(sentItem?.time) || Math.floor(Date.now() / 1000),
1043
+ timestampMs: Number(sentItem?.local_time ?? sentItem?.timestamp_ms ?? Date.now()),
1042
1044
  kind: String(sentItem?.message?.[0]?.type ?? 'text'),
1043
1045
  })
1044
1046
  }
@@ -456,6 +456,7 @@ export function satoriEventToOneBot(
456
456
  self_id: selfId,
457
457
  platform,
458
458
  sn: getNumber(event.sn),
459
+ timestamp: Math.floor(normalizeTimestampMs(event.timestamp) / 1000),
459
460
  timestamp_ms: getNumber(event.timestamp) ? normalizeTimestampMs(event.timestamp) : 0,
460
461
  time: Math.floor(normalizeTimestampMs(event.timestamp) / 1000),
461
462
  message_seq: getNumber(event.sn),
@@ -733,6 +734,7 @@ function messageListFromResponse(data: unknown): unknown[] {
733
734
  return {
734
735
  message_id: getString(message.id),
735
736
  sn: getNumber(message.sn),
737
+ timestamp: Math.floor(normalizeTimestampMs(message.timestamp) / 1000),
736
738
  timestamp_ms: getNumber(message.timestamp) ? normalizeTimestampMs(message.timestamp) : 0,
737
739
  time: Math.floor(normalizeTimestampMs(message.timestamp) / 1000),
738
740
  message_seq: getNumber(message.sn) || getNumber(message.seq),
@@ -518,13 +518,24 @@ function getBase64Source(item: any): string {
518
518
  return ''
519
519
  }
520
520
 
521
- function isBase64Source(source: string): boolean {
522
- return source.startsWith('base64://') || source.startsWith('data:')
523
- }
524
-
525
- export function getLocalMediaUrl(value: string): string {
526
- if (!value) return value
527
- const isLocalPath = value.startsWith('file:') ||
521
+ function isBase64Source(source: string): boolean {
522
+ return source.startsWith('base64://') || source.startsWith('data:')
523
+ }
524
+
525
+ function toPreviewImageSrc(item: any): string {
526
+ const source = String(item?.url ?? item?.file ?? '')
527
+ if (source.startsWith('base64://')) {
528
+ return `data:image/png;base64,${source.slice(9)}`
529
+ }
530
+ if (source.startsWith('data:') || source.startsWith('http:') || source.startsWith('https:')) {
531
+ return source
532
+ }
533
+ return getLocalMediaUrl(source)
534
+ }
535
+
536
+ export function getLocalMediaUrl(value: string): string {
537
+ if (!value) return value
538
+ const isLocalPath = value.startsWith('file:') ||
528
539
  /^[a-z]:[\\/]/i.test(value) ||
529
540
  value.startsWith('\\\\')
530
541
  if (!isLocalPath) return value
@@ -637,20 +648,15 @@ export async function sendMsgRaw(
637
648
  const preShowMsg = typeof msg === 'string'
638
649
  ? parsePreviewMarkup(msg)
639
650
  : JSON.parse(JSON.stringify(msg));
640
- preShowMsg.forEach((item: any) => {
641
- // 对 base64 图片做特殊处理
642
- if (item.type == 'image') {
643
- if (item.file.startsWith('base64://')) {
644
- const b64Str = (item.file as string).substring(9)
645
- item.url = 'data:image/png;base64,' + b64Str
646
- } else {
647
- item.url = item.file
648
- }
649
- }
650
- if (item.type == 'record' && item.file.startsWith('base64://')) {
651
- const b64Str = (item.file as string).substring(9)
652
- item.url = 'data:audio/webm;base64,' + b64Str
653
- }
651
+ preShowMsg.forEach((item: any) => {
652
+ // 对 base64 图片做特殊处理
653
+ if (item.type == 'image') {
654
+ item.url = toPreviewImageSrc(item)
655
+ }
656
+ if (item.type == 'record' && String(item?.file ?? '').startsWith('base64://')) {
657
+ const b64Str = (item.file as string).substring(9)
658
+ item.url = 'data:audio/webm;base64,' + b64Str
659
+ }
654
660
  })
655
661
  const showMsg = {
656
662
  revoke: true,
@@ -502,13 +502,6 @@
502
502
  <div><font-awesome-icon :icon="['fas', 'trash-can']" /></div>
503
503
  <a>{{ $t('移出群聊') }}</a>
504
504
  </div>
505
- <div v-show="tags.menuDisplay.config"
506
- @click="openChatInfoPan();
507
- ($refs.infoRef as any).openMoreConfig(selectedMsg?.sender.user_id);
508
- closeMsgMenu();">
509
- <div><font-awesome-icon :icon="['fas', 'cog']" /></div>
510
- <a>{{ $t('成员设置') }}</a>
511
- </div>
512
505
  <div v-show="tags.menuDisplay.jumpToMsg" @click="jumpSearchMsg">
513
506
  <div><font-awesome-icon :icon="['fas', 'arrow-up-right-from-square']" /></div>
514
507
  <a>{{ $t('跳转到消息') }}</a>
@@ -2089,6 +2082,8 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2089
2082
  forwardContent: msgBody,
2090
2083
  source: 'webui',
2091
2084
  sentAt: sentTime,
2085
+ timestamp: Math.floor(sentTime / 1000),
2086
+ timestampMs: sentTime,
2092
2087
  kind: 'forward',
2093
2088
  })
2094
2089
  }
@@ -2996,7 +2991,7 @@ function updateList(newLength: number, oldLength: number) {
2996
2991
  msgItem.type === 'image' &&
2997
2992
  msgItem.file != 'marketface'
2998
2993
  ) {
2999
- getImgList.push(msgItem.url)
2994
+ getImgList.push(msgItem.url || msgItem.file)
3000
2995
  }
3001
2996
  }
3002
2997
  }
package/lib/index.js CHANGED
@@ -5142,6 +5142,12 @@ function encodeKeyPart2(value) {
5142
5142
  return encodeURIComponent(value);
5143
5143
  }
5144
5144
  __name(encodeKeyPart2, "encodeKeyPart");
5145
+ function normalizeTimestampMs(value) {
5146
+ const num = Number(value ?? 0);
5147
+ if (!Number.isFinite(num) || num <= 0) return 0;
5148
+ return num > 1e12 ? num : num * 1e3;
5149
+ }
5150
+ __name(normalizeTimestampMs, "normalizeTimestampMs");
5145
5151
  function decodeKeyPart(value) {
5146
5152
  try {
5147
5153
  return decodeURIComponent(value);
@@ -5160,7 +5166,7 @@ function legacyMessagePrefix(platform, selfId, channelId) {
5160
5166
  __name(legacyMessagePrefix, "legacyMessagePrefix");
5161
5167
  function messageKey(record) {
5162
5168
  const time = String(
5163
- record.receivedAt ?? record.timestampMs ?? Number(record.timestamp) * 1e3
5169
+ normalizeTimestampMs(record.timestampMs ?? record.timestamp ?? record.receivedAt)
5164
5170
  ).padStart(16, "0");
5165
5171
  return `${messagePrefix(record.platform, record.selfId, record.channelId || "")}${time}:${encodeKeyPart2(record.id || "unknown")}`;
5166
5172
  }
@@ -5170,7 +5176,9 @@ function selfMessagePrefix(platform, selfId, channelId) {
5170
5176
  }
5171
5177
  __name(selfMessagePrefix, "selfMessagePrefix");
5172
5178
  function selfMessageKey(record) {
5173
- const time = String(record.sentAt).padStart(16, "0");
5179
+ const time = String(
5180
+ normalizeTimestampMs(record.timestampMs ?? record.timestamp ?? record.sentAt)
5181
+ ).padStart(16, "0");
5174
5182
  return `${selfMessagePrefix(record.platform, record.selfId, record.channelId)}${time}:${encodeKeyPart2(record.id || "unknown")}`;
5175
5183
  }
5176
5184
  __name(selfMessageKey, "selfMessageKey");
@@ -5824,7 +5832,7 @@ var _ChatDatabase = class _ChatDatabase {
5824
5832
  extractRecordTime(value) {
5825
5833
  try {
5826
5834
  const parsed = JSON.parse(value);
5827
- return Number(parsed.receivedAt ?? parsed.timestampMs ?? parsed.timestamp ?? parsed.sentAt ?? 0) || 0;
5835
+ return normalizeTimestampMs(parsed.timestampMs ?? parsed.timestamp ?? parsed.sentAt ?? parsed.receivedAt);
5828
5836
  } catch {
5829
5837
  return 0;
5830
5838
  }
@@ -5890,6 +5898,7 @@ var _Recorder = class _Recorder {
5890
5898
  const user = getObject(body.user);
5891
5899
  const sn = getNumber(body.sn);
5892
5900
  const timestamp = getNumber(body.timestamp) || Date.now();
5901
+ const timestampMs = timestamp > 1e12 ? timestamp : timestamp * 1e3;
5893
5902
  const record = {
5894
5903
  id: getString2(message2.id) || `satori-${sn}`,
5895
5904
  sequence: sn,
@@ -5900,7 +5909,7 @@ var _Recorder = class _Recorder {
5900
5909
  guildId: getString2(guild.id) || void 0,
5901
5910
  userId: getString2(user.id) || void 0,
5902
5911
  timestamp,
5903
- timestampMs: timestamp,
5912
+ timestampMs,
5904
5913
  receivedAt: Date.now(),
5905
5914
  content: getString2(message2.content) || getString2(message2.raw_message) || void 0,
5906
5915
  elements: Array.isArray(message2.elements) ? message2.elements : void 0,
@@ -7823,6 +7832,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
7823
7832
  const channelType = mode === "private" ? "user" : "group";
7824
7833
  const elements = this.normalizeElements(content);
7825
7834
  const contentText = typeof content === "string" ? content : import_koishi.h.toElementArray(content).join("");
7835
+ const sentAt = Date.now();
7826
7836
  const fingerprint = (0, import_node_crypto3.createHash)("sha256").update([
7827
7837
  platform,
7828
7838
  selfId,
@@ -7842,7 +7852,9 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
7842
7852
  message: toSegments(elements, (attrs) => this.resolveI18nElement(attrs)),
7843
7853
  forwardId: forwardId(elements),
7844
7854
  forwardContent: toForwardNodes(elements, (attrs) => this.resolveI18nElement(attrs)),
7845
- sentAt: Date.now(),
7855
+ sentAt,
7856
+ timestamp: Math.floor(sentAt / 1e3),
7857
+ timestampMs: sentAt,
7846
7858
  sequence: ++this.sequence,
7847
7859
  source,
7848
7860
  kind: detectKind(elements),
@@ -7932,6 +7944,7 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
7932
7944
  const webPublic = import_node_path3.default.resolve(__dirname, "..", "client", "web", "public");
7933
7945
  const webIndex = import_node_path3.default.resolve(__dirname, "..", "client", "web", "index.html");
7934
7946
  const uploadDir = import_node_path3.default.resolve(ctx.baseDir, "data", "chat-patch", "upload-media");
7947
+ const mediaDir = import_node_path3.default.resolve(ctx.baseDir, "data", "chat-patch", "media");
7935
7948
  const cacheType = /* @__PURE__ */ __name((type) => type === "user" ? "friend" : type, "cacheType");
7936
7949
  const getVite = /* @__PURE__ */ __name(() => {
7937
7950
  return ctx.console.vite;
@@ -7981,6 +7994,20 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
7981
7994
  const historyChannelCandidates = /* @__PURE__ */ __name((channelId) => {
7982
7995
  return channelId ? [channelId] : [];
7983
7996
  }, "historyChannelCandidates");
7997
+ const messageSortTime = /* @__PURE__ */ __name((item) => {
7998
+ const timestampMs = Number(item.timestampMs ?? 0);
7999
+ if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1e3;
8000
+ const timestamp = Number(item.timestamp ?? 0);
8001
+ if (timestamp > 0) return timestamp * 1e3;
8002
+ return Number(item.receivedAt ?? 0);
8003
+ }, "messageSortTime");
8004
+ const selfMessageSortTime = /* @__PURE__ */ __name((item) => {
8005
+ const timestampMs = Number(item.timestampMs ?? item.sentAt ?? 0);
8006
+ if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1e3;
8007
+ const timestamp = Number(item.timestamp ?? 0);
8008
+ if (timestamp > 0) return timestamp * 1e3;
8009
+ return item.sentAt;
8010
+ }, "selfMessageSortTime");
7984
8011
  const mimeToExt = {
7985
8012
  "audio/mpeg": ".mp3",
7986
8013
  "audio/mp3": ".mp3",
@@ -8105,26 +8132,9 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8105
8132
  localPath: filePath
8106
8133
  };
8107
8134
  });
8108
- ctx.server.get(`${config.basePath}/api/media`, async (koa) => {
8109
- const fileName = import_node_path3.default.basename(String(koa.query.file ?? koa.query.name ?? ""));
8110
- if (!fileName || fileName === "." || fileName === "..") {
8111
- koa.status = 400;
8112
- koa.body = { error: "missing media file" };
8113
- return;
8114
- }
8115
- const filePath = import_node_path3.default.resolve(uploadDir, fileName);
8116
- const root = import_node_path3.default.resolve(uploadDir);
8117
- if (filePath !== root && !filePath.startsWith(`${root}${import_node_path3.default.sep}`)) {
8118
- koa.status = 400;
8119
- koa.body = { error: "invalid media file" };
8120
- return;
8121
- }
8122
- if (!(0, import_node_fs3.existsSync)(filePath) || !(0, import_node_fs3.statSync)(filePath).isFile()) {
8123
- koa.status = 404;
8124
- koa.body = { error: "media file not found" };
8125
- return;
8126
- }
8135
+ const sendLocalFile = /* @__PURE__ */ __name(async (koa, filePath) => {
8127
8136
  const size = (0, import_node_fs3.statSync)(filePath).size;
8137
+ const fileName = import_node_path3.default.basename(filePath);
8128
8138
  koa.set("Accept-Ranges", "bytes");
8129
8139
  if (fileName.toLowerCase().endsWith(".webm")) {
8130
8140
  koa.type = "audio/webm";
@@ -8164,6 +8174,23 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8164
8174
  }
8165
8175
  koa.set("Content-Length", String(size));
8166
8176
  koa.body = (0, import_node_fs3.createReadStream)(filePath);
8177
+ }, "sendLocalFile");
8178
+ ctx.server.get(`${config.basePath}/api/media`, async (koa) => {
8179
+ const fileName = import_node_path3.default.basename(String(koa.query.file ?? koa.query.name ?? ""));
8180
+ if (!fileName || fileName === "." || fileName === "..") {
8181
+ koa.status = 400;
8182
+ koa.body = { error: "missing media file" };
8183
+ return;
8184
+ }
8185
+ for (const root of [uploadDir, mediaDir]) {
8186
+ const filePath = import_node_path3.default.resolve(root, fileName);
8187
+ if (filePath !== root && filePath.startsWith(`${root}${import_node_path3.default.sep}`) && (0, import_node_fs3.existsSync)(filePath) && (0, import_node_fs3.statSync)(filePath).isFile()) {
8188
+ await sendLocalFile(koa, filePath);
8189
+ return;
8190
+ }
8191
+ }
8192
+ koa.status = 404;
8193
+ koa.body = { error: "media file not found" };
8167
8194
  });
8168
8195
  ctx.server.get(`${config.basePath}/api/cache/all`, async (koa) => {
8169
8196
  const entries = await database.getAllContacts();
@@ -8215,21 +8242,21 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8215
8242
  if (selfMessages.length) break;
8216
8243
  }
8217
8244
  messages.sort((a, b) => {
8218
- const timeA = Number(a?.receivedAt ?? a?.timestampMs ?? a?.timestamp ?? 0);
8219
- const timeB = Number(b?.receivedAt ?? b?.timestampMs ?? b?.timestamp ?? 0);
8245
+ const timeA = messageSortTime(a);
8246
+ const timeB = messageSortTime(b);
8220
8247
  return timeA - timeB;
8221
8248
  });
8222
- selfMessages.sort((a, b) => a.sentAt - b.sentAt);
8249
+ selfMessages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b));
8223
8250
  const combined = [];
8224
8251
  for (const item of messages) {
8225
8252
  combined.push({
8226
8253
  kind: "message",
8227
- time: Number(item?.receivedAt ?? item?.timestampMs ?? item?.timestamp ?? 0),
8254
+ time: messageSortTime(item),
8228
8255
  record: item
8229
8256
  });
8230
8257
  }
8231
8258
  for (const item of selfMessages) {
8232
- combined.push({ kind: "self", time: item.sentAt, record: item });
8259
+ combined.push({ kind: "self", time: selfMessageSortTime(item), record: item });
8233
8260
  }
8234
8261
  combined.sort((a, b) => a.time - b.time);
8235
8262
  const capped = combined.slice(-limit);
@@ -8255,7 +8282,7 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8255
8282
  messages = Number.isFinite(beforeTime) && beforeTime > 0 ? await database.listSelfMessagesBefore(platform, selfId, candidate, beforeTime, limit) : await database.listSelfMessages(platform, selfId, candidate, limit);
8256
8283
  if (messages.length) break;
8257
8284
  }
8258
- messages.sort((a, b) => a.sentAt - b.sentAt);
8285
+ messages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b));
8259
8286
  koa.body = { messages };
8260
8287
  });
8261
8288
  ctx.server.post(`${config.basePath}/api/self-messages`, async (koa) => {
@@ -8275,6 +8302,8 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8275
8302
  forwardId: typeof body.forwardId === "string" ? body.forwardId : void 0,
8276
8303
  forwardContent: Array.isArray(body.forwardContent) ? body.forwardContent : void 0,
8277
8304
  sentAt: typeof body.sentAt === "number" && Number.isFinite(body.sentAt) ? body.sentAt : Date.now(),
8305
+ timestamp: typeof body.timestamp === "number" && Number.isFinite(body.timestamp) ? body.timestamp : void 0,
8306
+ timestampMs: typeof body.timestampMs === "number" && Number.isFinite(body.timestampMs) ? body.timestampMs : void 0,
8278
8307
  sequence: typeof body.sequence === "number" && Number.isFinite(body.sequence) ? body.sequence : 0,
8279
8308
  source: body.source === "plugin" ? "plugin" : "webui",
8280
8309
  kind: typeof body.kind === "string" ? body.kind : "text",
@@ -8286,6 +8315,7 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8286
8315
  koa.body = { error: "missing self-message params" };
8287
8316
  return;
8288
8317
  }
8318
+ const sentAt = payload.sentAt ?? Date.now();
8289
8319
  const record = {
8290
8320
  id: payload.id || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`,
8291
8321
  platform: payload.platform,
@@ -8299,7 +8329,9 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8299
8329
  message: payload.message,
8300
8330
  forwardId: payload.forwardId,
8301
8331
  forwardContent: payload.forwardContent,
8302
- sentAt: payload.sentAt ?? Date.now(),
8332
+ sentAt,
8333
+ timestamp: payload.timestamp ?? Math.floor(sentAt / 1e3),
8334
+ timestampMs: payload.timestampMs ?? sentAt,
8303
8335
  sequence: payload.sequence ?? 0,
8304
8336
  source: payload.source ?? "webui",
8305
8337
  kind: payload.kind ?? "text",