Monorepo for Tangled tangled.org
4

Configure Feed

Select the types of activity you want to include in your feed.

1package models 2 3import ( 4 "fmt" 5 "strings" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 securejoin "github.com/cyphar/filepath-securejoin" 10 enry "github.com/go-enry/go-enry/v2" 11 "tangled.org/core/api/tangled" 12) 13 14type Repo struct { 15 Id int64 16 Did string 17 Name string 18 Knot string 19 Rkey string 20 Created time.Time 21 Description string 22 Website string 23 Topics []string 24 Spindle string 25 Labels []string 26 RepoDid string 27 28 // optionally, populate this when querying for reverse mappings 29 RepoStats *RepoStats 30 31 // optional 32 Source string 33} 34 35func (r *Repo) AsRecord() tangled.Repo { 36 var source, spindle, description, website *string 37 38 if r.Source != "" { 39 source = &r.Source 40 } 41 42 if r.Spindle != "" { 43 spindle = &r.Spindle 44 } 45 46 if r.Description != "" { 47 description = &r.Description 48 } 49 50 if r.Website != "" { 51 website = &r.Website 52 } 53 54 var repoDid *string 55 if r.RepoDid != "" { 56 repoDid = &r.RepoDid 57 } 58 59 return tangled.Repo{ 60 Knot: r.Knot, 61 Name: r.cosmeticName(), 62 Description: description, 63 Website: website, 64 Topics: r.Topics, 65 CreatedAt: r.Created.Format(time.RFC3339), 66 Source: source, 67 Spindle: spindle, 68 Labels: r.Labels, 69 RepoDid: repoDid, 70 } 71} 72 73func (r *Repo) cosmeticName() *string { 74 if r.Name == "" || r.Name == r.Rkey { 75 return nil 76 } 77 return &r.Name 78} 79 80func (r Repo) RepoAt() syntax.ATURI { 81 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 82} 83 84func (r Repo) Slug() string { 85 if r.Name != "" { 86 return r.Name 87 } 88 return r.Rkey 89} 90 91func (r Repo) RepoIdentifier() string { 92 if r.RepoDid != "" { 93 return r.RepoDid 94 } 95 p, _ := securejoin.SecureJoin(r.Did, r.Rkey) 96 return p 97} 98 99func (r Repo) PinIdentifier() string { 100 if r.RepoDid != "" { 101 return r.RepoDid 102 } 103 return string(r.RepoAt()) 104} 105 106func (r Repo) TopicStr() string { 107 return strings.Join(r.Topics, " ") 108} 109 110type RepoStats struct { 111 Language string 112 StarCount int 113 IssueCount IssueCount 114 PullCount PullCount 115 ForkCount int 116} 117 118// returns the first file extension for the language ("ts" for typescript) as 119// an uppercase string 120func (s *RepoStats) LangShortName() string { 121 if s == nil || s.Language == "" { 122 return "" 123 } 124 exts := enry.GetLanguageExtensions(s.Language) 125 if len(exts) > 0 { 126 // extensions include the leading dot, e.g. ".ts" -> "TS" 127 return strings.ToUpper(strings.TrimPrefix(exts[0], ".")) 128 } 129 return s.Language 130} 131 132type IssueCount struct { 133 Open int 134 Closed int 135} 136 137type PullCount struct { 138 Open int 139 Merged int 140 Closed int 141 Deleted int 142} 143 144type RepoLabel struct { 145 Id int64 146 RepoDid syntax.DID 147 LabelAt syntax.ATURI 148} 149 150var reservedRepoNames = map[string]struct{}{ 151 "self": {}, 152} 153 154func ValidateRepoName(name string) error { 155 if len(name) == 0 { 156 return fmt.Errorf("Repository name cannot be empty") 157 } 158 if len(name) > 100 { 159 return fmt.Errorf("Repository name must be 100 characters or fewer") 160 } 161 162 // check for path traversal attempts 163 if strings.Contains(name, "/") || strings.Contains(name, "\\") { 164 return fmt.Errorf("Repository name contains invalid path characters") 165 } 166 167 // check for sequences that could be used for traversal when normalized 168 if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 169 return fmt.Errorf("Repository name contains invalid path sequence") 170 } 171 172 // then continue with character validation 173 for _, char := range name { 174 if !((char >= 'a' && char <= 'z') || 175 (char >= 'A' && char <= 'Z') || 176 (char >= '0' && char <= '9') || 177 char == '-' || char == '_' || char == '.') { 178 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 179 } 180 } 181 182 // additional check to prevent multiple sequential dots 183 if strings.Contains(name, "..") { 184 return fmt.Errorf("Repository name cannot contain sequential dots") 185 } 186 187 if _, reserved := reservedRepoNames[strings.ToLower(name)]; reserved { 188 return fmt.Errorf("Repository name %q is reserved", name) 189 } 190 191 // if all checks pass 192 return nil 193} 194 195func StripGitExt(name string) string { 196 return strings.TrimSuffix(name, ".git") 197} 198 199type RepoGroup struct { 200 Repo *Repo 201 Issues []Issue 202} 203 204type BlobContentType int 205 206const ( 207 BlobContentTypeCode BlobContentType = iota 208 BlobContentTypeMarkup 209 BlobContentTypeImage 210 BlobContentTypeSvg 211 BlobContentTypeVideo 212 BlobContentTypeSubmodule 213 BlobContentTypeOther 214) 215 216func (ty BlobContentType) IsCode() bool { return ty == BlobContentTypeCode } 217func (ty BlobContentType) IsMarkup() bool { return ty == BlobContentTypeMarkup } 218func (ty BlobContentType) IsImage() bool { return ty == BlobContentTypeImage } 219func (ty BlobContentType) IsSvg() bool { return ty == BlobContentTypeSvg } 220func (ty BlobContentType) IsVideo() bool { return ty == BlobContentTypeVideo } 221func (ty BlobContentType) IsSubmodule() bool { return ty == BlobContentTypeSubmodule } 222func (ty BlobContentType) HasTextView() bool { 223 return ty == BlobContentTypeCode || ty == BlobContentTypeMarkup || ty == BlobContentTypeSvg 224} 225func (ty BlobContentType) HasRenderedView() bool { 226 return ty != BlobContentTypeCode && ty != BlobContentTypeOther 227} 228func (ty BlobContentType) HasRawView() bool { 229 return ty != BlobContentTypeSubmodule 230} 231 232type BlobView struct { 233 // content type flags 234 ContentType BlobContentType 235 236 // Content data 237 ContentSrc string // URL to raw content 238 Contents string // textual content 239 FileTooLarge bool // textual content is too large 240 Lines int // line count of textual content 241 SizeHint uint64 242}