social bookmarking for atproto
at main 4.4 kB view raw
1/* 2 * clippr: a social bookmarking service for the AT Protocol 3 * Copyright (c) 2025 clippr contributors. 4 * SPDX-License-Identifier: AGPL-3.0-only 5 */ 6 7import { 8 SocialClipprActorProfile, 9 SocialClipprFeedClip, 10 SocialClipprFeedTag, 11} from "@clipprjs/lexicons"; 12import { 13 isDatetime, 14 isGenericUri, 15 isLanguageCode, 16} from "@atcute/lexicons/syntax"; 17import Logger from "../logger.js"; 18import { ComAtprotoRepoStrongRef } from "@atcute/atproto"; 19import { is } from "@atcute/lexicons"; 20 21export async function validateProfile( 22 record: SocialClipprActorProfile.Main, 23): Promise<boolean> { 24 if (!isDatetime(record.createdAt)) { 25 Logger.verbose( 26 "Invalid createdAt timestamp for incoming profile record", 27 record, 28 ); 29 return false; 30 } 31 32 if (record.displayName) { 33 if (record.displayName.length > 64) { 34 Logger.verbose( 35 "Too long displayName from incoming profile record", 36 record, 37 ); 38 return false; 39 } 40 41 if (record.displayName.length < 1) { 42 Logger.verbose( 43 "Too short displayName from incoming profile record", 44 record, 45 ); 46 return false; 47 } 48 } else { 49 Logger.verbose("No displayName from incoming profile record", record); 50 return false; 51 } 52 53 if (record.description) { 54 if (record.description.length > 500) { 55 Logger.verbose( 56 "Too long description from incoming profile record", 57 record, 58 ); 59 return false; 60 } 61 62 if (record.description.length < 1) { 63 Logger.verbose( 64 "Too short description from incoming profile record", 65 record, 66 ); 67 return false; 68 } 69 } 70 71 return true; 72} 73 74export async function validateTag( 75 record: SocialClipprFeedTag.Main, 76): Promise<boolean> { 77 if (!isDatetime(record.createdAt)) { 78 Logger.verbose( 79 "Invalid createdAt timestamp for incoming tag record", 80 record, 81 ); 82 return false; 83 } 84 85 if (record.name.length > 64) { 86 Logger.verbose("Name from incoming tag record is too long", record); 87 return false; 88 } 89 90 if (record.color) { 91 if (record.color.length > 7) { 92 Logger.verbose("Color from incoming tag record is too long", record); 93 return false; 94 } 95 96 if (!record.color.match("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")) { 97 Logger.verbose( 98 "Invalid hexadecimal color for incoming tag record", 99 record, 100 ); 101 return false; 102 } 103 } 104 105 if (record.description) { 106 if (record.description.length > 500) { 107 Logger.verbose( 108 "Description from incoming tag record is too long", 109 record, 110 ); 111 return false; 112 } 113 } 114 115 return true; 116} 117 118export async function validateClip( 119 record: SocialClipprFeedClip.Main, 120): Promise<boolean> { 121 if (!isGenericUri(record.url)) { 122 Logger.verbose("Invalid url from incoming clip record", record); 123 return false; 124 } 125 126 if (record.url.length > 2000) { 127 Logger.verbose("Too long url from incoming clip record", record); 128 return false; 129 } 130 131 if (record.title.length > 2048) { 132 Logger.verbose("Too long title from incoming clip record", record); 133 return false; 134 } 135 136 if (record.description.length > 4096) { 137 Logger.verbose("Too long description from incoming clip record", record); 138 return false; 139 } 140 141 if (record.notes) { 142 if (record.notes.length > 10000) { 143 Logger.verbose("Too long notes from incoming clip record", record); 144 return false; 145 } 146 } 147 148 if (record.tags) { 149 if ( 150 record.tags.some((tag) => { 151 return tag.$type !== "com.atproto.repo.strongRef"; 152 }) 153 ) { 154 Logger.verbose( 155 "A tag from incoming clip record is not typed as strongRef", 156 record, 157 ); 158 return false; 159 } 160 161 if ( 162 record.tags.some((tag) => { 163 return !is(ComAtprotoRepoStrongRef.mainSchema, tag); 164 }) 165 ) { 166 Logger.verbose( 167 "A tag from incoming clip record is not a valid strongRef", 168 record, 169 ); 170 return false; 171 } 172 173 // There should definitely be more tests here, but I'm not exactly sure what to add... 174 } 175 176 if (typeof record.unlisted !== "boolean") { 177 Logger.verbose( 178 "Unlisted value from incoming clip record is not a boolean", 179 record, 180 ); 181 return false; 182 } 183 184 // Same with "unread" but it's not required so 185 186 if (record.languages) { 187 if (record.languages.some((lang) => !isLanguageCode(lang))) { 188 Logger.verbose( 189 "An item in the incoming clip record's languages array is not a valid language code", 190 record, 191 ); 192 return false; 193 } 194 } 195 196 if (!isDatetime(record.createdAt)) { 197 Logger.verbose( 198 "Invalid createdAt timestamp for incoming clip record", 199 record, 200 ); 201 return false; 202 } 203 204 return true; 205}