From a667f2898437c1721bfca65b0ab60d0fb2016759 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 25 Aug 2025 14:47:12 +0200 Subject: [PATCH 01/20] Send proof hint flags for media messages --- src/models/attachmentManager.ts | 3 ++- src/models/proof.ts | 8 +++++++- src/plugins/utils.js | 9 +++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/models/attachmentManager.ts b/src/models/attachmentManager.ts index 6d029fc..ac0566f 100644 --- a/src/models/attachmentManager.ts +++ b/src/models/attachmentManager.ts @@ -627,7 +627,8 @@ export const createUploadBatch = (manager: AttachmentManager | null, room: Room }, eventId, attachment.dimensions, - attachment.thumbnail + attachment.thumbnail, + attachment.proofHintFlags ) .then((mediaEventId: string) => { // Look at last item rotation, flipping the sign on this, so looks more like a true stack diff --git a/src/models/proof.ts b/src/models/proof.ts index deee838..2340d3a 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -67,6 +67,8 @@ export type ProofHintFlags = { export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefined) => { if (!proof) return undefined; + let foundCreated = false; + let screenshot = false; let camera = false; let aiGenerated = false; @@ -104,10 +106,14 @@ export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefine ) { aiGenerated = true; } - return; + foundCreated = true; + break; } } } + if (foundCreated) { + break; + } } if (valid) { const flags: ProofHintFlags = { diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 1231e49..500b03b 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -23,6 +23,7 @@ export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file"; export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel"; export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type"; +export const CLIENT_EVENT_PROOF_HINT = "im.keanu.proof_hint"; export const THUMBNAIL_MAX_WIDTH = 640; export const THUMBNAIL_MAX_HEIGHT = 640; @@ -410,7 +411,7 @@ class Util { return [encryptedBytes, encryptedFile]; } - sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail) { + sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail, proofHintFlags) { const uploadPromise = new UploadPromise(undefined); uploadPromise.wrappedPromise = new Promise((resolve, reject) => { var reader = new FileReader(); @@ -485,12 +486,16 @@ class Util { msgtype = "m.video"; } - var messageContent = { + let messageContent = { body: description, info: info, msgtype: msgtype, }; + if (proofHintFlags) { + messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags); + } + // If thread root (an eventId) is set, add that here if (threadRoot) { messageContent["m.relates_to"] = { From 46cab38b8a849e7acd3c14c7637f323376946385 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 28 Aug 2025 10:27:57 +0200 Subject: [PATCH 02/20] Work on RE engine for proof flags --- src/models/proof.ts | 318 ++++++++++++++++++++++++++++++++------------ 1 file changed, 230 insertions(+), 88 deletions(-) diff --git a/src/models/proof.ts b/src/models/proof.ts index 2340d3a..a3019ff 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -2,129 +2,271 @@ export type AIInferenceResult = { aiGenerated: boolean; aiProbability: number; humanProbability: number; -} +}; export const C2PASourceTypeScreenCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture"; export const C2PASourceTypeDigitalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"; export const C2PASourceTypeComputationalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"; export const C2PASourceTypeCompositeCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture"; -export const C2PASourceTypeTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"; -export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"; +export const C2PASourceTypeTrainedAlgorithmicMedia = + "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"; +export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia = + "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"; export type C2PAActionsAssertion = { - actions: { - action: string; - softwareAgent?: string; - digitalSourceType?: string; - }[]; -} + actions: { + action: string; + softwareAgent?: string; + digitalSourceType?: string; + }[]; +}; export type C2PAAssertion = { - label: string; - data: C2PAActionsAssertion | undefined; -} + label: string; + data: C2PAActionsAssertion | undefined; +}; export type C2PAManifest = { - assertions: C2PAAssertion[]; - signature_info: { - time: string; - } -} + assertions: C2PAAssertion[]; + signature_info: { + time: string; + }; +}; export type C2PAValidationResults = { - activeManifest?: { - failure: any[]; - success: any[]; - informational: any[]; - } -} + activeManifest?: { + failure: any[]; + success: any[]; + informational: any[]; + }; +}; export type C2PAManifestInfo = { - active_manifest: string; - manifests: {[key: string]: C2PAManifest}; - validation_results?: C2PAValidationResults; -} + active_manifest: string; + manifests: { [key: string]: C2PAManifest }; + validation_results?: C2PAValidationResults; +}; export type C2PAData = { - manifest_info: C2PAManifestInfo; -} + manifest_info: C2PAManifestInfo; +}; export type Proof = { - data?: any; - name?: string; - json?: string; - integrity?: { pgp?: any; c2pa?: C2PAData; exif?: {[key: string]: string | Object}; opentimestamps?: any }; - ai?: { inferenceResult?: AIInferenceResult}; -} + data?: any; + name?: string; + json?: string; + integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any }; + ai?: { inferenceResult?: AIInferenceResult }; +}; export type ProofHintFlags = { - aiGenerated?: boolean; - aiEdited?: boolean; - screenshot?: boolean; - camera?: boolean; -} + aiGenerated?: boolean; + aiEdited?: boolean; + screenshot?: boolean; + camera?: boolean; +}; -export const extractProofHintFlags = (proof?: Proof): (ProofHintFlags | undefined) => { - if (!proof) return undefined; +type FlagMatchRule = { + field: string; + match: string[]; + description: string; +}; - let foundCreated = false; +type FlagMatchInfo = { + field: string; + value: string; + re: string; +}; - let screenshot = false; - let camera = false; - let aiGenerated = false; - let aiEdited = false; - let valid = false; +type FlagMatch = { + re: RegExp; + value: string; +}; - try { - let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest; - if (results) { - valid = results.failure.length == 0 && results.success.length > 0; +const ruleScreenshot = (): FlagMatchRule[] => { + return [ + { + field: + "integrity/c2pa/manifest_info/manifests[]/assertions[]/{label=c2pa.actions}/data/actions[]/{action=c2pa.created}/digitalSourceType", + match: [C2PASourceTypeScreenCapture], + description: "Screen capture", + }, + ]; +}; + +const aiHintFlags = (): FlagMatchRule[] => { + const knownAIServices = [ + "ChatGPT", + "OpenAI-API", + "Adobe Firefly", + "RunwayML", + "Runway AI", + "Google AI", + "Stable Diffusion", + ]; + return [ + { field: "name", match: ["^DALL_E_", "^Gen-3"], description: "File name" }, + { + field: "integrity/c2pa/manifest_info/manifests[]/claim_generator", + match: knownAIServices, + description: "C2PA claim generator", + }, + { field: "iptc/Credit", match: knownAIServices, description: "IPTC Credit" }, + { field: "iptc/Provider", match: knownAIServices, description: "IPTC Provider" }, + { field: "iptc/ImageSupplier[]", match: knownAIServices, description: "IPTC ImageSupplier" }, + { field: "iptc/ImageCreator[]", match: knownAIServices, description: "IPTC ImageCreator" }, + ]; +}; + +const matchFlag = (rules: FlagMatchRule[], file: any) => { + const matchParts = (parts: string[], o: any, re: RegExp[]): FlagMatch[] | undefined => { + if (parts.length == 0 || o == undefined) return undefined; + let part = parts[0]; + if (part.endsWith("[]")) { + part = part.substring(0, part.length - 2); + if (o[part] != undefined) { + let opart: any[] = o[part]; + if (!Array.isArray(opart)) { + opart = Object.values(opart) ?? []; } + if (parts.length == 1) { + let strings = opart as string[]; + if (!strings) return undefined; + return strings.reduce((res: FlagMatch[] | undefined, s: any) => { + return re.reduce((res: FlagMatch[] | undefined, r: any) => { + if (r.test(s)) { + const r2 = res || []; + r2.push({ re: r, value: s as string }); + return r2; + } + return res; + }, res); + }, undefined); + } else { + return opart.reduce((res: FlagMatch[] | undefined, o: any) => { + let matches = matchParts(parts.slice(1), o, re); + if (matches) { + const r2 = res || []; + r2.push(...matches); + return r2; + } + return res; + }, undefined); + } + } else { + return undefined; + } + } else if (part.startsWith("{") && part.endsWith("}")) { + const [prop, val] = part.substring(1, part.length - 1).split("="); + if (o[prop] === val) { + return matchParts(parts.slice(1), o, re); + } else { + return undefined; + } + } else { + if (o[part] != undefined) { + if (parts.length == 1) { + return re.reduce((res: FlagMatch[] | undefined, r: any) => { + if (r.test(o[part])) { + const r2 = res || []; + r2.push({ re: r, value: o[part] as string }); + return r2; + } + return res; + }, undefined); + } else { + return matchParts(parts.slice(1), o[part], re); + } + } else { + return undefined; + } + } + }; - const manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {}); - for (const manifest of manifests) { - for (const assertion of manifest.assertions) { - if (assertion.label === "c2pa.actions") { - const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? []; - const a = actions.find((a) => a.action === "c2pa.created"); - if (a) { + let result = false; + let resultInfo: FlagMatchInfo[] = []; + for (let rule of rules) { + try { + const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi")); + let parts = rule.field.split("/"); + let matches = matchParts(parts, file, re); + if (matches) { + matches.forEach((m) => { + resultInfo.push({ field: rule.description, value: m.value, re: m.re.source }); + }); + result = true; + } + } catch (e) { + console.error("Invalid RE", e); + } + } + return { result: result, matches: resultInfo }; +}; + +export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { + if (!proof) return undefined; + + let foundCreated = false; + + let screenshot = false; + let camera = false; + let aiGenerated = false; + let aiEdited = false; + let valid = false; + + try { + let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest; + if (results) { + valid = results.failure.length == 0 && results.success.length > 0; + } + + const manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {}); + for (const manifest of manifests) { + for (const assertion of manifest.assertions) { + if (assertion.label === "c2pa.actions") { + const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? []; + const a = actions.find((a) => a.action === "c2pa.created"); + if (a) { // creator.value = a.softwareAgent; // dateCreated.value = dayjs(Date.parse(manifest.signature_info.time)); - if (a.digitalSourceType === C2PASourceTypeScreenCapture) { - screenshot = true; - } - if ( - a.digitalSourceType === C2PASourceTypeDigitalCapture || - a.digitalSourceType === C2PASourceTypeComputationalCapture || - a.digitalSourceType === C2PASourceTypeCompositeCapture - ) { - camera = true; - } - if ( - a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia || - a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia - ) { - aiGenerated = true; - } - foundCreated = true; - break; + if (a.digitalSourceType === C2PASourceTypeScreenCapture) { + screenshot = true; } + if ( + a.digitalSourceType === C2PASourceTypeDigitalCapture || + a.digitalSourceType === C2PASourceTypeComputationalCapture || + a.digitalSourceType === C2PASourceTypeCompositeCapture + ) { + camera = true; + } + if ( + a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia || + a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia + ) { + aiGenerated = true; + } + foundCreated = true; + break; } } if (foundCreated) { break; } } - if (valid) { - const flags: ProofHintFlags = { - aiGenerated, - aiEdited, - screenshot, - camera - }; - return flags; + if (foundCreated) { + break; } - } catch (error) { } - return undefined; -}; \ No newline at end of file + if (valid) { + screenshot = matchFlag(ruleScreenshot(), proof).result; + const flags: ProofHintFlags = { + aiGenerated, + aiEdited, + screenshot, + camera, + }; + return flags; + } + } catch (error) {} + return undefined; +}; From 2d4143fde62a472b7c8058c22c22f232a6fe7eb3 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 1 Sep 2025 16:26:05 +0200 Subject: [PATCH 03/20] Use RE for proof extraxtion --- src/models/proof.ts | 118 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 10 deletions(-) diff --git a/src/models/proof.ts b/src/models/proof.ts index a3019ff..679d0e8 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -72,6 +72,11 @@ type FlagMatchRule = { description: string; }; +type FlagMatchRuleValue = { + path: string; + value: string; +}; + type FlagMatchInfo = { field: string; value: string; @@ -87,13 +92,35 @@ const ruleScreenshot = (): FlagMatchRule[] => { return [ { field: - "integrity/c2pa/manifest_info/manifests[]/assertions[]/{label=c2pa.actions}/data/actions[]/{action=c2pa.created}/digitalSourceType", + "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType", match: [C2PASourceTypeScreenCapture], description: "Screen capture", }, ]; }; +const ruleCamera = (): FlagMatchRule[] => { + return [ + { + field: + "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType", + match: [C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture], + description: "Captured by camera", + }, + ]; +}; + +const ruleAiGenerated = (): FlagMatchRule[] => { + return [ + { + field: + "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/digitalSourceType", + match: [C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia], + description: "Generated by AI", + }, + ]; +}; + const aiHintFlags = (): FlagMatchRule[] => { const knownAIServices = [ "ChatGPT", @@ -122,13 +149,22 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => { const matchParts = (parts: string[], o: any, re: RegExp[]): FlagMatch[] | undefined => { if (parts.length == 0 || o == undefined) return undefined; let part = parts[0]; - if (part.endsWith("[]")) { - part = part.substring(0, part.length - 2); + const lastBracket = part.lastIndexOf("["); + if (part.endsWith("]") && lastBracket > 0) { + const optionalConstraint = part.substring(lastBracket + 1, part.length - 1); + part = part.substring(0, lastBracket); if (o[part] != undefined) { let opart: any[] = o[part]; if (!Array.isArray(opart)) { opart = Object.values(opart) ?? []; } + + // Any constraints controlling what array object(s) to consider? + if (optionalConstraint) { + const [prop, val] = optionalConstraint.split("="); + opart = opart.filter((item) => item[prop] === val); + } + if (parts.length == 1) { let strings = opart as string[]; if (!strings) return undefined; @@ -156,13 +192,6 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => { } else { return undefined; } - } else if (part.startsWith("{") && part.endsWith("}")) { - const [prop, val] = part.substring(1, part.length - 1).split("="); - if (o[prop] === val) { - return matchParts(parts.slice(1), o, re); - } else { - return undefined; - } } else { if (o[part] != undefined) { if (parts.length == 1) { @@ -203,6 +232,71 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => { return { result: result, matches: resultInfo }; }; +const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => { + const getValues = (path: string[], objectPath: any[], actualPath: string, o: any): FlagMatchRuleValue[] | undefined => { + if (path.length == 0 || o == undefined) return undefined; + let part = path[0]; + const lastBracket = part.lastIndexOf("["); + if (part === "..") { + return getValues(path.slice(1), objectPath.slice(1), actualPath + "/..", objectPath[0]); + } + if (part.endsWith("]") && lastBracket > 0) { + const optionalConstraint = part.substring(lastBracket + 1, part.length - 1); + part = part.substring(0, lastBracket); + if (o[part] != undefined) { + let opart: any[] = o[part]; + if (!Array.isArray(opart)) { + opart = Object.values(opart) ?? []; + } + + // Any constraints controlling what array object(s) to consider? + if (optionalConstraint) { + const [prop, val] = optionalConstraint.split("="); + opart = opart.filter((item) => item[prop] === val); + } + + if (path.length == 1) { + let strings = opart as string[]; + return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s })); + } else { + return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => { + const newObjectPaths = [o, ...objectPath]; + let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o); + if (matches) { + const r2 = res || []; + r2.push(...matches); + return r2; + } + return res; + }, undefined); + } + } else { + return undefined; + } + } else { + if (o[part] != undefined) { + if (path.length == 1) { + return [{ path: actualPath + "/" + part, value: o[part] }]; + } else { + const newObjectPaths = [o, ...objectPath]; + return getValues(path.slice(1), newObjectPaths, actualPath + "/" + part, o[part]); + } + } else { + return undefined; + } + } + }; + + let result: FlagMatchRuleValue[] = []; + try { + let parts = flagPath.split("/"); + result = getValues(parts, [], "", file) ?? []; + } catch (e) { + console.error("Invalid RE", e); + } + return result; +}; + export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { if (!proof) return undefined; @@ -258,6 +352,10 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined } } if (valid) { + + const dateCreated = extractFlagValues("integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/issuer", proof); + console.error("DATE CREATED", dateCreated); + screenshot = matchFlag(ruleScreenshot(), proof).result; const flags: ProofHintFlags = { aiGenerated, From dd76692640b866b68eeae2df44b6dc01e85d5d5e Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 4 Sep 2025 14:20:45 +0200 Subject: [PATCH 04/20] Cleanup proof hint flag code somewhat --- src/models/proof.ts | 139 ++++---------------------------------------- 1 file changed, 12 insertions(+), 127 deletions(-) diff --git a/src/models/proof.ts b/src/models/proof.ts index 679d0e8..b00012f 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -83,11 +83,6 @@ type FlagMatchInfo = { re: string; }; -type FlagMatch = { - re: RegExp; - value: string; -}; - const ruleScreenshot = (): FlagMatchRule[] => { return [ { @@ -146,85 +141,19 @@ const aiHintFlags = (): FlagMatchRule[] => { }; const matchFlag = (rules: FlagMatchRule[], file: any) => { - const matchParts = (parts: string[], o: any, re: RegExp[]): FlagMatch[] | undefined => { - if (parts.length == 0 || o == undefined) return undefined; - let part = parts[0]; - const lastBracket = part.lastIndexOf("["); - if (part.endsWith("]") && lastBracket > 0) { - const optionalConstraint = part.substring(lastBracket + 1, part.length - 1); - part = part.substring(0, lastBracket); - if (o[part] != undefined) { - let opart: any[] = o[part]; - if (!Array.isArray(opart)) { - opart = Object.values(opart) ?? []; - } - - // Any constraints controlling what array object(s) to consider? - if (optionalConstraint) { - const [prop, val] = optionalConstraint.split("="); - opart = opart.filter((item) => item[prop] === val); - } - - if (parts.length == 1) { - let strings = opart as string[]; - if (!strings) return undefined; - return strings.reduce((res: FlagMatch[] | undefined, s: any) => { - return re.reduce((res: FlagMatch[] | undefined, r: any) => { - if (r.test(s)) { - const r2 = res || []; - r2.push({ re: r, value: s as string }); - return r2; - } - return res; - }, res); - }, undefined); - } else { - return opart.reduce((res: FlagMatch[] | undefined, o: any) => { - let matches = matchParts(parts.slice(1), o, re); - if (matches) { - const r2 = res || []; - r2.push(...matches); - return r2; - } - return res; - }, undefined); - } - } else { - return undefined; - } - } else { - if (o[part] != undefined) { - if (parts.length == 1) { - return re.reduce((res: FlagMatch[] | undefined, r: any) => { - if (r.test(o[part])) { - const r2 = res || []; - r2.push({ re: r, value: o[part] as string }); - return r2; - } - return res; - }, undefined); - } else { - return matchParts(parts.slice(1), o[part], re); - } - } else { - return undefined; - } - } - }; - let result = false; let resultInfo: FlagMatchInfo[] = []; for (let rule of rules) { try { const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi")); - let parts = rule.field.split("/"); - let matches = matchParts(parts, file, re); - if (matches) { - matches.forEach((m) => { - resultInfo.push({ field: rule.description, value: m.value, re: m.re.source }); + const values = extractFlagValues(rule.field, file); + values.forEach((v) => { + re.forEach((r) => { + if (r.test(v.value)) { + resultInfo.push({ field: v.path, value: v.value, re: r.source }) + } }); - result = true; - } + }) } catch (e) { console.error("Invalid RE", e); } @@ -300,11 +229,6 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { if (!proof) return undefined; - let foundCreated = false; - - let screenshot = false; - let camera = false; - let aiGenerated = false; let aiEdited = false; let valid = false; @@ -313,55 +237,16 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined if (results) { valid = results.failure.length == 0 && results.success.length > 0; } - - const manifests = Object.values(proof.integrity?.c2pa?.manifest_info.manifests ?? {}); - for (const manifest of manifests) { - for (const assertion of manifest.assertions) { - if (assertion.label === "c2pa.actions") { - const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? []; - const a = actions.find((a) => a.action === "c2pa.created"); - if (a) { - // creator.value = a.softwareAgent; - // dateCreated.value = dayjs(Date.parse(manifest.signature_info.time)); - if (a.digitalSourceType === C2PASourceTypeScreenCapture) { - screenshot = true; - } - if ( - a.digitalSourceType === C2PASourceTypeDigitalCapture || - a.digitalSourceType === C2PASourceTypeComputationalCapture || - a.digitalSourceType === C2PASourceTypeCompositeCapture - ) { - camera = true; - } - if ( - a.digitalSourceType === C2PASourceTypeTrainedAlgorithmicMedia || - a.digitalSourceType === C2PASourceTypeCompositeWithTrainedAlgorithmicMedia - ) { - aiGenerated = true; - } - foundCreated = true; - break; - } - } - if (foundCreated) { - break; - } - } - if (foundCreated) { - break; - } - } if (valid) { - const dateCreated = extractFlagValues("integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/issuer", proof); - console.error("DATE CREATED", dateCreated); + const dateCreated = extractFlagValues("integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time", proof); + console.log("DATE CREATED", dateCreated); - screenshot = matchFlag(ruleScreenshot(), proof).result; const flags: ProofHintFlags = { - aiGenerated, + aiGenerated: matchFlag(ruleAiGenerated(), proof).result, aiEdited, - screenshot, - camera, + screenshot: matchFlag(ruleScreenshot(), proof).result, + camera: matchFlag(ruleCamera(), proof).result, }; return flags; } From 66eef037e02238b6877fa2e0d649df59c6145650 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Fri, 5 Sep 2025 11:16:50 +0200 Subject: [PATCH 05/20] More work on sending/reading proof hint flags --- src/assets/css/sendattachments.scss | 37 +++- src/assets/translations/en.json | 12 +- .../content-credentials/C2PAInfo.vue | 147 ++++++-------- .../content-credentials/CCProperty.vue | 23 +++ .../content-credentials/EXIFInfo.vue | 18 +- src/components/file_mode/AttachmentInfo.vue | 2 +- src/components/file_mode/GalleryItemsView.vue | 182 +++++++++--------- .../composition/MessageThreadSending.vue | 13 -- src/models/proof.ts | 52 +++-- 9 files changed, 267 insertions(+), 219 deletions(-) create mode 100644 src/components/content-credentials/CCProperty.vue diff --git a/src/assets/css/sendattachments.scss b/src/assets/css/sendattachments.scss index 4ffd699..1aac19a 100644 --- a/src/assets/css/sendattachments.scss +++ b/src/assets/css/sendattachments.scss @@ -482,7 +482,8 @@ $hiliteColor: #4642f1; } .send-attachments-info-popup { - background-color: rgba(0, 0, 0, 0.9); + background-color: rgba(0, 0, 0, 0.8); + border-radius: 24px 24px 0 0; .done-button { padding: 14px 24px; @@ -621,6 +622,20 @@ $hiliteColor: #4642f1; } } + .detail-info { + color: #333333; + background-color: #DAD9FC; + padding: 16px; + margin: 16px 0 16px 0; + border-radius: 8px 8px 0 8px; + font-family: "Inter", sans-serif; + font-weight: 400; + font-size: 14px; + line-height: 125%; + letter-spacing: 0.4px; + vertical-align: middle; + } + .detail-row { margin-top: 12px; font-family: "Inter", sans-serif; @@ -629,9 +644,29 @@ $hiliteColor: #4642f1; line-height: 125%; letter-spacing: 0.4px; color: rgba(255, 255, 255, 0.9); + display: flex; .v-icon { + padding: 9.33px; margin-right: 8px; + width: 32px; + height: 32px; + background-color: black; + } + + .detail-row__text { + display: flex; + flex-direction: column; + } + + .detail-row__title { + color: #dad9fc; + font-family: "Inter", sans-serif; + font-weight: 400; + font-size: 12px; + line-height: 125%; + letter-spacing: 0.4px; + vertical-align: middle; } } } diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index ab3763e..fa95640 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -504,8 +504,16 @@ "content_credentials_info": "Source or history information is available for this media to be verified.", "learn_more": "Learn more", "ai_used": "Photo modified with AI", + "screenshot": "Screenshot. ", "screenshot_taken_on": "Screenshot taken on {date}", - "captured_with_camera": "Captured with a camera", - "old_photo": "Photo older than 3 months" + "captured_with_camera": "Captured with a real camera. ", + "captured_screenshot": "Screenshot. ", + "captured_screenshot_ago": "Screenshot captured {ago} ago. ", + "generated_with_ai": "Generated with AI. ", + "generated_with_ai_ago": "Generated with AI {ago} ago. ", + "old_photo": "Photo older than 3 months. ", + "cc_source": "Source", + "cc_capture_timestamp": "Capture Timestamp", + "cc_location": "Location" } } diff --git a/src/components/content-credentials/C2PAInfo.vue b/src/components/content-credentials/C2PAInfo.vue index 4df016b..7d1e5c8 100644 --- a/src/components/content-credentials/C2PAInfo.vue +++ b/src/components/content-credentials/C2PAInfo.vue @@ -1,132 +1,99 @@ diff --git a/src/components/content-credentials/EXIFInfo.vue b/src/components/content-credentials/EXIFInfo.vue index 5f3b32f..2ec59d7 100644 --- a/src/components/content-credentials/EXIFInfo.vue +++ b/src/components/content-credentials/EXIFInfo.vue @@ -3,16 +3,13 @@
{{ t("file_mode.exif_data") }}
-
- $vuetify.icons.ic_exif_time{{ dateTime }} -
-
- $vuetify.icons.ic_exif_location{{ location }} -
-
- $vuetify.icons.ic_exif_device_camera{{ makeAndModel }} -
- + + + + + @@ -20,6 +17,7 @@ import { computed } from "vue"; import dayjs from "dayjs"; import { useI18n } from "vue-i18n"; +import CCProperty from "./CCProperty.vue"; const { t } = useI18n(); diff --git a/src/components/file_mode/AttachmentInfo.vue b/src/components/file_mode/AttachmentInfo.vue index 2fdfa91..54c624b 100644 --- a/src/components/file_mode/AttachmentInfo.vue +++ b/src/components/file_mode/AttachmentInfo.vue @@ -28,7 +28,7 @@ - + diff --git a/src/components/file_mode/GalleryItemsView.vue b/src/components/file_mode/GalleryItemsView.vue index 82dd87e..352bd71 100644 --- a/src/components/file_mode/GalleryItemsView.vue +++ b/src/components/file_mode/GalleryItemsView.vue @@ -1,24 +1,33 @@ - @@ -125,7 +131,7 @@ export default { bottom: 21px; width: 34px; height: 34px; - background: rgba(255,255,255,0.8); + background: rgba(255, 255, 255, 0.8); border-radius: 17px; display: flex; align-items: center; @@ -133,13 +139,13 @@ export default { } .fill-screen { - position: fixed !important; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: black; - z-index: 20 !important; - justify-content: space-between !important; + position: fixed !important; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: black; + z-index: 20 !important; + justify-content: space-between !important; } - \ No newline at end of file + diff --git a/src/components/messages/composition/MessageThreadSending.vue b/src/components/messages/composition/MessageThreadSending.vue index ce44c0d..43707f8 100644 --- a/src/components/messages/composition/MessageThreadSending.vue +++ b/src/components/messages/composition/MessageThreadSending.vue @@ -38,13 +38,6 @@ - @@ -52,7 +45,6 @@ import MessageOutgoing from "./MessageOutgoing.vue"; import { MessageProps, useMessage } from "./useMessage"; import { ROOM_TYPE_CHANNEL } from "@/plugins/utils"; -import GalleryItemsView from "../../file_mode/GalleryItemsView.vue"; import ThumbnailView from "../../file_mode/ThumbnailView.vue"; import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue"; import { inject, ref, Ref, unref, watch } from "vue"; @@ -65,7 +57,6 @@ const $$sanitize: any = inject("globalSanitize"); let items: Ref = ref([]); const layoutedItems: Ref<{ size: number; item: Attachment }[]> = ref([]); -const showItem: Ref = ref(undefined); const uploadBatch: Ref = ref(undefined); @@ -93,10 +84,6 @@ const retryUpload = () => { } } -const onItemClick = (event: any) => { - showItem.value = event.item; -}; - const layout = () => { if (!items.value || items.value.length == 0) { layoutedItems.value = []; diff --git a/src/models/proof.ts b/src/models/proof.ts index b00012f..f71d27a 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -60,6 +60,9 @@ export type Proof = { }; export type ProofHintFlags = { + valid: boolean; + source?: string; + creationDate?: Date; aiGenerated?: boolean; aiEdited?: boolean; screenshot?: boolean; @@ -150,10 +153,11 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => { values.forEach((v) => { re.forEach((r) => { if (r.test(v.value)) { - resultInfo.push({ field: v.path, value: v.value, re: r.source }) + result = true; + resultInfo.push({ field: v.path, value: v.value, re: r.source }); } }); - }) + }); } catch (e) { console.error("Invalid RE", e); } @@ -162,7 +166,12 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => { }; const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => { - const getValues = (path: string[], objectPath: any[], actualPath: string, o: any): FlagMatchRuleValue[] | undefined => { + const getValues = ( + path: string[], + objectPath: any[], + actualPath: string, + o: any + ): FlagMatchRuleValue[] | undefined => { if (path.length == 0 || o == undefined) return undefined; let part = path[0]; const lastBracket = part.lastIndexOf("["); @@ -189,7 +198,7 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s })); } else { return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => { - const newObjectPaths = [o, ...objectPath]; + const newObjectPaths = [o, ...objectPath]; let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o); if (matches) { const r2 = res || []; @@ -237,19 +246,34 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined if (results) { valid = results.failure.length == 0 && results.success.length > 0; } - if (valid) { - const dateCreated = extractFlagValues("integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time", proof); - console.log("DATE CREATED", dateCreated); + const source = extractFlagValues( + "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/softwareAgent", + proof + ); - const flags: ProofHintFlags = { - aiGenerated: matchFlag(ruleAiGenerated(), proof).result, - aiEdited, - screenshot: matchFlag(ruleScreenshot(), proof).result, - camera: matchFlag(ruleCamera(), proof).result, - }; - return flags; + const dateCreated = extractFlagValues( + "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time", + proof + ); + let date: Date | undefined = undefined; + if (dateCreated && dateCreated.length == 1) { + try { + date = new Date(Date.parse(dateCreated[0].value)); + } catch (error) {} } + console.log("DATE CREATED", date); + + const flags: ProofHintFlags = { + valid: valid, + source: source && source.length == 1 ? source[0].value : undefined, + creationDate: date, + aiGenerated: matchFlag(ruleAiGenerated(), proof).result, + aiEdited, + screenshot: matchFlag(ruleScreenshot(), proof).result, + camera: matchFlag(ruleCamera(), proof).result, + }; + return flags; } catch (error) {} return undefined; }; From 46479d4c37afeb649cd97be2c181c182d7cd79da Mon Sep 17 00:00:00 2001 From: N-Pex Date: Fri, 5 Sep 2025 13:48:52 +0200 Subject: [PATCH 06/20] Support for running proof check on client side --- .../file_mode/EventAttachmentInfo.vue | 61 ++++++++++++++++++ src/components/file_mode/GalleryItemsView.vue | 64 +++++++++++-------- src/models/attachmentManager.ts | 6 +- src/models/eventAttachment.ts | 3 + src/plugins/proofmode.ts | 16 +++++ src/plugins/proofmodeWorker.js | 5 +- 6 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 src/components/file_mode/EventAttachmentInfo.vue diff --git a/src/components/file_mode/EventAttachmentInfo.vue b/src/components/file_mode/EventAttachmentInfo.vue new file mode 100644 index 0000000..46fbb89 --- /dev/null +++ b/src/components/file_mode/EventAttachmentInfo.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/file_mode/GalleryItemsView.vue b/src/components/file_mode/GalleryItemsView.vue index 352bd71..4476f3b 100644 --- a/src/components/file_mode/GalleryItemsView.vue +++ b/src/components/file_mode/GalleryItemsView.vue @@ -5,62 +5,76 @@ arrow_back
{{ displayDate }}
- info_outline + info_outline more_vert
+ + + +
{{ $t("menu.done") }}
+ +
+ + + + +
+
diff --git a/src/components/content-credentials/CCSummary.vue b/src/components/content-credentials/CCSummary.vue new file mode 100644 index 0000000..a064a7a --- /dev/null +++ b/src/components/content-credentials/CCSummary.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/file_mode/EventAttachmentInfo.vue b/src/components/file_mode/EventAttachmentInfo.vue index 46fbb89..57739e9 100644 --- a/src/components/file_mode/EventAttachmentInfo.vue +++ b/src/components/file_mode/EventAttachmentInfo.vue @@ -5,6 +5,7 @@ +
{{ t("cc.metadata-stripped") }}
@@ -19,23 +20,29 @@ import { computed, onMounted, Ref, ref } from "vue"; import { EventAttachment } from "../../models/eventAttachment"; import proofmode from "../../plugins/proofmode"; import { extractProofHintFlags } from "../../models/proof"; +import { useI18n } from "vue-i18n"; + +const { t } = useI18n(); const { attachment } = defineProps<{ attachment?: EventAttachment; }>(); const loadingProof: Ref = ref(false); +const metaStripped: Ref = ref(false); onMounted(() => { if (attachment?.proofHintFlags && attachment.proof === undefined) { const a = attachment; loadingProof.value = true; + metaStripped.value = true; a.loadSrc() .then((data) => { if (data && data.data) { return proofmode.proofCheckSource(data.data).then((res) => { a.proof = res; a.proofHintFlags = extractProofHintFlags(a.proof); + metaStripped.value = a?.proof?.integrity?.c2pa === undefined && a?.proof?.integrity?.exif === undefined; }); } }) @@ -43,6 +50,8 @@ onMounted(() => { .finally(() => { loadingProof.value = false; }); + } else { + metaStripped.value = attachment?.proof?.integrity?.c2pa === undefined && attachment?.proof?.integrity?.exif === undefined; } }); @@ -58,4 +67,5 @@ const hasExif = computed(() => { diff --git a/src/components/file_mode/GalleryItemsView.vue b/src/components/file_mode/GalleryItemsView.vue index 4476f3b..2c0494e 100644 --- a/src/components/file_mode/GalleryItemsView.vue +++ b/src/components/file_mode/GalleryItemsView.vue @@ -105,7 +105,7 @@ watch(props.items, (newValue: EventAttachment[], oldValue: EventAttachment[]) => // Added or removed? if (newValue && oldValue && newValue.length > oldValue.length) { currentAttachment.value = newValue[oldValue.length]; - } else if (newValue) { + } else if (newValue && oldValue && newValue.length < oldValue.length) { currentAttachment.value = newValue[newValue.length - 1]; } }); @@ -119,6 +119,7 @@ const downloadOne = () => { const downloadAll = () => { props.items.forEach((item) => util.download($matrix.matrixClient, $matrix.useAuthedMedia, item.event)); }; + \ No newline at end of file From dfa354bf74a5ad3c151fe8a803144400f977cca5 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Wed, 10 Sep 2025 18:06:41 +0200 Subject: [PATCH 15/20] Fix sending audio and video files --- src/components/RoomExport.vue | 9 ++ src/components/chatMixin.js | 4 +- src/components/file_mode/ThumbnailView.vue | 6 +- .../messages/composition/MessageThread.vue | 6 +- src/plugins/utils.js | 97 +++++++++---------- src/services/audio.service.js | 9 +- 6 files changed, 66 insertions(+), 65 deletions(-) diff --git a/src/components/RoomExport.vue b/src/components/RoomExport.vue index c9728a4..eda85df 100644 --- a/src/components/RoomExport.vue +++ b/src/components/RoomExport.vue @@ -393,6 +393,7 @@ export default { break; case "image/gif": extension = ".gif"; + break; } let fileName = event.getId() + extension; @@ -407,6 +408,14 @@ export default { } } else if (mime.startsWith("audio/")) { var extension = ".webm"; + switch (mime) { + case "audio/mpeg": + extension = ".mp3"; + break; + case "audio/x-m4a": + extension = ".m4a"; + break; + } let fileName = event.getId() + extension; audioFolder.file(fileName, blob); // TODO calc bytes let elements = comp.el.getElementsByTagName("audio"); diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js index 5ed5410..85bee75 100644 --- a/src/components/chatMixin.js +++ b/src/components/chatMixin.js @@ -208,7 +208,7 @@ export default { if (isForExport) { return MessageIncomingVideoExport; } - return MessageVideo; + return MessageThread; } else if (event.getContent().msgtype == "m.file") { if (isForExport) { return MessageIncomingFileExport; @@ -255,7 +255,7 @@ export default { if (isForExport) { return MessageOutgoingVideoExport; } - return MessageVideo; + return MessageThread; } else if (event.getContent().msgtype == "m.file") { if (isForExport) { return MessageOutgoingFileExport; diff --git a/src/components/file_mode/ThumbnailView.vue b/src/components/file_mode/ThumbnailView.vue index c405a3d..7922ee4 100644 --- a/src/components/file_mode/ThumbnailView.vue +++ b/src/components/file_mode/ThumbnailView.vue @@ -64,7 +64,7 @@ const poster: Ref = ref(undefined); const updateSource = () => { if (isEventAttachment(props.item)) { const eventAttachment = props.item; - if (isVideo.value || eventAttachment.src) { + if (eventAttachment.src) { source.value = eventAttachment.src; } else if (previewOnly) { eventAttachment.loadThumbnail().then((url) => { @@ -74,6 +74,10 @@ const updateSource = () => { eventAttachment.loadSrc().then((url) => { source.value = url.data; }) + } else if (isVideo.value) { + eventAttachment.loadSrc().then((url) => { + source.value = url.data; + }) } } else if (isAttachment(props.item)) { const attachment = props.item; diff --git a/src/components/messages/composition/MessageThread.vue b/src/components/messages/composition/MessageThread.vue index f85ca25..05b2f4b 100644 --- a/src/components/messages/composition/MessageThread.vue +++ b/src/components/messages/composition/MessageThread.vue @@ -128,7 +128,7 @@ watch( event, () => { if (event.value) { - if (event.value.getContent().msgtype == "m.image") { + if (["m.image", "m.video"].includes(event.value.getContent().msgtype ?? "")) { // Single image mode items.value = [event.value].map((e: MatrixEvent) => { let ea = $matrix.attachmentManager.getEventAttachment(e); @@ -157,14 +157,14 @@ onBeforeUnmount(() => { }); const showMessageText = computed((): boolean => { - if (event.value?.getContent().msgtype == "m.image") { + if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) { return false; } return true; }); const showMultiview = computed((): boolean => { - if (event.value?.getContent().msgtype == "m.image") { + if (["m.image", "m.video"].includes(event.value?.getContent().msgtype ?? "")) { return true; } return ( diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 500b03b..a980545 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -47,21 +47,6 @@ class Util { return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread"; } - getAttachmentUrlAndDuration(event) { - return new Promise((resolve, reject) => { - const content = event.getContent(); - if (content.url != null) { - resolve([content.url, content.info.duration]); - return; - } - if (content.file && content.file.url) { - resolve([content.file.url, content.info.duration]); - } else { - reject("No url found!"); - } - }); - } - getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) { return new Promise((resolve, reject) => { const content = event.getContent(); @@ -577,7 +562,7 @@ class Util { // Generate audio waveforms if (msgtype == "m.audio") { - this.generateWaveform(fileContents, messageContent); + await this.generateWaveform(fileContents, messageContent); } const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent); @@ -594,46 +579,56 @@ class Util { return uploadPromise; } - generateWaveform(data, messageContent) { + async generateWaveform(data, messageContent) { if (!(window.AudioContext || window.webkitAudioContext)) { return; // No support } - const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - if (audioCtx) { - return audioCtx.decodeAudioData(data).then((audioBuffer) => { - const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel - const samples = 1000; // Number of samples - const blockSize = Math.floor(rawData.length / samples); - let filteredData = []; - for (let i = 0; i < samples; i++) { - let blockStart = blockSize * i; // the location of the first sample in the block - let sum = 0; - for (let j = 0; j < blockSize; j++) { - sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block + try { + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + if (audioCtx) { + const audioBuffer = await audioCtx.decodeAudioData(data); + if (audioBuffer) { + const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel + const samples = 1000; // Number of samples + const blockSize = Math.floor(rawData.length / samples); + let filteredData = []; + for (let i = 0; i < samples; i++) { + let blockStart = blockSize * i; // the location of the first sample in the block + let sum = 0; + for (let j = 0; j < blockSize; j++) { + sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block + } + filteredData.push(sum / blockSize); // divide the sum by the block size to get the average + } + + // Normalize + const multiplier = Math.pow(Math.max(...filteredData), -1); + filteredData = filteredData.map((n) => n * multiplier); + + // Integerize + filteredData = filteredData.map((n) => parseInt((n * 255).toFixed())); + + // Generate SVG of waveform + let svg = ``; + svg += ``; + svg += ""; + + messageContent.format = "org.matrix.custom.html"; + messageContent.formatted_body = svg; + + // if duration is not set, do that here, since we have it + if (!messageContent.info.duration) { + messageContent.info.duration = parseInt((1000 * audioBuffer.duration).toFixed()); } - filteredData.push(sum / blockSize); // divide the sum by the block size to get the average } - - // Normalize - const multiplier = Math.pow(Math.max(...filteredData), -1); - filteredData = filteredData.map((n) => n * multiplier); - - // Integerize - filteredData = filteredData.map((n) => parseInt((n * 255).toFixed())); - - // Generate SVG of waveform - let svg = ``; - svg += ``; - svg += ""; - - messageContent.format = "org.matrix.custom.html"; - messageContent.formatted_body = svg; - }); + } + } catch (error) { + return; } } diff --git a/src/services/audio.service.js b/src/services/audio.service.js index 2c291f1..586df82 100644 --- a/src/services/audio.service.js +++ b/src/services/audio.service.js @@ -46,14 +46,7 @@ export default { this.infoMap.set(eventId, entry); // Get duration information - utils - .getAttachmentUrlAndDuration(event) - .then(([ignoredurl, duration]) => { - entry.duration = duration; - }) - .catch((err) => { - console.error("Failed to fetch attachment duration: ", err); - }); + entry.duration = event.getContent()?.info?.duration ?? 0; } entry.listeners.add(uid); return entry; From 25ef8bcbcd73b3b642f823c906a8ac79a8ea6afe Mon Sep 17 00:00:00 2001 From: N-Pex Date: Wed, 10 Sep 2025 18:07:01 +0200 Subject: [PATCH 16/20] Build 70 --- package.json | 2 +- package.json.bak | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b61c774..1742c5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.69", + "version": "0.1.70", "private": true, "scripts": { "dev": "vite", diff --git a/package.json.bak b/package.json.bak index c659d3d..b61c774 100644 --- a/package.json.bak +++ b/package.json.bak @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.68", + "version": "0.1.69", "private": true, "scripts": { "dev": "vite", From 714154323beb8c7e03b9b74366352ef00358f614 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 11 Sep 2025 15:26:59 +0200 Subject: [PATCH 17/20] Fix "retry" --- src/components/messages/composition/MessageThreadSending.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/messages/composition/MessageThreadSending.vue b/src/components/messages/composition/MessageThreadSending.vue index 43707f8..f07893f 100644 --- a/src/components/messages/composition/MessageThreadSending.vue +++ b/src/components/messages/composition/MessageThreadSending.vue @@ -80,7 +80,7 @@ const cancelUpload = () => { const retryUpload = () => { if (uploadBatch.value) { - uploadBatch.value.send(uploadBatch.value.sendingRootMessage.value ?? ""); + uploadBatch.value.send(uploadBatch.value.sendingRootMessage?.value ?? ""); } } From 1bffe443bcc024441ce1c0bdef118be5f026faef Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 11 Sep 2025 16:00:18 +0200 Subject: [PATCH 18/20] Make sure body is set --- src/plugins/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/utils.js b/src/plugins/utils.js index a980545..a1f0b91 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -223,7 +223,7 @@ class Util { } sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) { - var content = ContentHelpers.makeTextMessage(text); + var content = ContentHelpers.makeTextMessage(text ?? ""); if (editedEvent) { content["m.relates_to"] = { rel_type: "m.replace", From be7aa6b0cac858d6e5e985c96049571219878414 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 11 Sep 2025 16:02:04 +0200 Subject: [PATCH 19/20] Build 71 Fix lint errors --- package-lock.json | 81 +++++++++++++++++++++++++++-------------------- package.json | 2 +- package.json.bak | 2 +- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fc0bde..685198d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "keanuapp-weblite", - "version": "0.1.59", + "version": "0.1.70", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "keanuapp-weblite", - "version": "0.1.59", + "version": "0.1.70", "dependencies": { "@guardianproject/proofmode": "^0.4.0", "@matrix-org/olm": "^3.2.12", @@ -3073,12 +3073,16 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/clean-insights-sdk": { @@ -6535,15 +6539,23 @@ "dev": true }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -7367,9 +7379,9 @@ "dev": true }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -7441,9 +7453,9 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", - "integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz", + "integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==", "dev": true, "license": "MIT", "dependencies": { @@ -9914,12 +9926,12 @@ "peer": true }, "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" } }, "clean-insights-sdk": { @@ -12358,12 +12370,13 @@ "dev": true }, "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" } }, "shebang-command": { @@ -12921,9 +12934,9 @@ "dev": true }, "vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "requires": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -12948,9 +12961,9 @@ } }, "vite-plugin-static-copy": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", - "integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz", + "integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==", "dev": true, "requires": { "chokidar": "^3.5.3", diff --git a/package.json b/package.json index 1742c5e..852555b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.70", + "version": "0.1.71", "private": true, "scripts": { "dev": "vite", diff --git a/package.json.bak b/package.json.bak index b61c774..1742c5e 100644 --- a/package.json.bak +++ b/package.json.bak @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.69", + "version": "0.1.70", "private": true, "scripts": { "dev": "vite", From 3eca0ce41e3e0552b187dfa37f1caa40ed2a52fb Mon Sep 17 00:00:00 2001 From: N-Pex Date: Thu, 11 Sep 2025 17:04:41 +0200 Subject: [PATCH 20/20] Build 72 Don't send proof hint flags for now. --- package.json | 2 +- package.json.bak | 2 +- src/plugins/utils.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 852555b..646fe36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.71", + "version": "0.1.72", "private": true, "scripts": { "dev": "vite", diff --git a/package.json.bak b/package.json.bak index 1742c5e..852555b 100644 --- a/package.json.bak +++ b/package.json.bak @@ -1,6 +1,6 @@ { "name": "keanuapp-weblite", - "version": "0.1.70", + "version": "0.1.71", "private": true, "scripts": { "dev": "vite", diff --git a/src/plugins/utils.js b/src/plugins/utils.js index a1f0b91..ff65834 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -477,9 +477,9 @@ class Util { msgtype: msgtype, }; - if (proofHintFlags) { - messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags); - } + // if (proofHintFlags) { + // messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags); + // } // If thread root (an eventId) is set, add that here if (threadRoot) {