AuraGem Servers > Commit [8079e6e]
Wed March 20, 2024 5:48 AM -0500
Add in Ask and Star Wars database. Lots of other changes. .gitignore | 2 +- .vscode/launch.json | 15 +++++++++++++++ gemini/ask/ask.go | 1435 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/ask/db.go | 348 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/ask/types.go | 410 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/chat/chat.go | 13 +++++++++++-- gemini/devlog.go | 16 ++++++++++------ gemini/gemini.go | 156 ++++++++++++++++++----------------------------------- gemini/music/music.go | 242 +++++++++++++++++++++++++++++++++++++++++++---------- gemini/music/music_index.gmi | 27 +++++++++++++++++++++++++++ gemini/music/music_index_scroll.scroll | 27 +++++++++++++++++++++++++++ gemini/music/radio.go | 12 ++++++++++++ gemini/music/stations.go | 38 ++++++++++++++++++++++++++++++++++++++ gemini/search/feedback.go | 7 ++++++- gemini/search/search.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++---- gemini/starwars/queries.go | 326 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/starwars/starwars.go | 406 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/starwars/tables.go | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/starwars/types.go | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++++ gemini/textola/textola.go | 8 +++++++- gemini/texts/christianity/christianity.go | 6 ++++++ gemini/texts/islam/islam.go | 2 ++ gemini/texts/judaism/judaism.go | 1 + gemini/texts/judaism/sefaria.go | 13 ++++++++----- gemini/texts/judaism/sefaria_test.go | 30 ++++++++++++++++++++++++++++++ gemini/utils/atom.go | 4 ++-- gemini/weather.go | 6 ++++++ gemini/youtube/utils.go | 4 ++++ gemini/youtube/youtube.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++----- go.mod | 42 ++++++++++++++++++++++-------------------- go.sum | 145 +++++++++++++++++++++++++++++++++++++---------------- main.go | 32 ++++++++++++++++++++++++++++++++
Commit Hash: 8079e6e55c830102f1b541d762083d98267c2808
Tree Hash: a18fa9317d94673e4b28f02fe59da273e1214671
Date: 2024-03-20T05:48:35-05:00
Changes
.gitignore
... | ...
7 | data/
8 | access.log
9 | *.pem
10 | *.key
11 | *.crt
12 | *.exe
13 | auragem_sis
14 | config/config.go
15 | main
16 | main.backup
17 | SIS/
- 12 |
+ 12 | *.prof
.vscode/launch.json (created)
+ 1 | {
+ 2 | // Use IntelliSense to learn about possible attributes.
+ 3 | // Hover to view descriptions of existing attributes.
+ 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ 5 | "version": "0.2.0",
+ 6 | "configurations": [
+ 7 | {
+ 8 | "name": "Launch Package",
+ 9 | "type": "go",
+ 10 | "request": "launch",
+ 11 | "mode": "auto",
+ 12 | "program": "${workspaceFolder}"
+ 13 | }
+ 14 | ]
+ 15 | }
gemini/ask/ask.go (created)
+ 1 | package ask
+ 2 |
+ 3 | import (
+ 4 | "database/sql"
+ 5 | "errors"
+ 6 | "fmt"
+ 7 | "net/url"
+ 8 | "strconv"
+ 9 | "strings"
+ 10 | "time"
+ 11 | "unicode"
+ 12 | "unicode/utf8"
+ 13 |
+ 14 | gemini "git.sr.ht/~adnano/go-gemini"
+ 15 | gemini2 "github.com/clseibold/go-gemini"
+ 16 | "gitlab.com/clseibold/auragem_sis/db"
+ 17 | sis "gitlab.com/clseibold/smallnetinformationservices"
+ 18 | )
+ 19 |
+ 20 | var registerNotification = `# AuraGem Ask
+ 21 |
+ 22 | You have selected a certificate that has not been registered yet. Registering associates a username to your certificate and allows you to start posting. Please register here:
+ 23 |
+ 24 | => /~ask/?register Register Cert
+ 25 | `
+ 26 |
+ 27 | func HandleAsk(s sis.ServerHandle) {
+ 28 | conn := db.NewConn(db.AskDB)
+ 29 | conn.SetMaxOpenConns(500)
+ 30 | conn.SetMaxIdleConns(3)
+ 31 | conn.SetConnMaxLifetime(time.Hour * 4)
+ 32 |
+ 33 | s.AddRoute("/ask", func(request sis.Request) {
+ 34 | request.Redirect("/~ask/")
+ 35 | })
+ 36 | s.AddRoute("/~ask", func(request sis.Request) {
+ 37 | request.Redirect("/~ask/")
+ 38 | })
+ 39 | s.AddRoute("/~ask/", func(request sis.Request) {
+ 40 | cert := request.UserCert
+ 41 | query, err2 := request.Query()
+ 42 | if err2 != nil {
+ 43 | return
+ 44 | }
+ 45 |
+ 46 | if cert == nil {
+ 47 | if query == "register" || query != "" {
+ 48 | request.Redirect("Please enable a certificate.")
+ 49 | return
+ 50 | } else {
+ 51 | getHomepage(request, conn)
+ 52 | }
+ 53 | } else {
+ 54 | fmt.Printf("%s\n", query)
+ 55 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 56 | if !isRegistered {
+ 57 | if query == "register" {
+ 58 | request.RequestInput("Enter a username:")
+ 59 | return
+ 60 | } else if query != "" {
+ 61 | // Do registration
+ 62 | RegisterUser(request, conn, query, request.UserCertHash_Gemini())
+ 63 | } else {
+ 64 | request.Gemini(registerNotification)
+ 65 | return
+ 66 | //return getUserDashboard(c, conn, user)
+ 67 | }
+ 68 | } else {
+ 69 | if query == "register" {
+ 70 | request.Redirect("/~ask/")
+ 71 | } else {
+ 72 | getUserDashboard(request, conn, user)
+ 73 | }
+ 74 | }
+ 75 | }
+ 76 | })
+ 77 |
+ 78 | s.AddRoute("/~ask/register", func(request sis.Request) {
+ 79 | cert := request.UserCert
+ 80 |
+ 81 | if cert == nil {
+ 82 | request.Redirect("/~ask/?register")
+ 83 | } else {
+ 84 | // Check if user already exists, if so, give error.
+ 85 | _, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 86 | if isRegistered {
+ 87 | request.TemporaryFailure("You are already registered. Be sure to select your certificate on the homepage.")
+ 88 | return
+ 89 | }
+ 90 |
+ 91 | query, err2 := request.Query()
+ 92 | if err2 != nil {
+ 93 | return
+ 94 | } else if query == "" {
+ 95 | //request.RequestInput("Enter a username:")
+ 96 | request.Redirect("/~ask/?register")
+ 97 | } else {
+ 98 | // Do registration
+ 99 | RegisterUser(request, conn, query, request.UserCertHash_Gemini())
+ 100 | }
+ 101 | }
+ 102 | })
+ 103 |
+ 104 | s.AddRoute("/~ask/recent", func(request sis.Request) {
+ 105 | var recentQuestionsBuilder strings.Builder
+ 106 |
+ 107 | activities := getRecentActivity_Questions(conn)
+ 108 | prevYear, prevMonth, prevDay := 0, time.Month(1), 0
+ 109 | for _, activity := range activities {
+ 110 | year, month, day := activity.Activity_date.Date()
+ 111 | if prevYear != 0 && (year != prevYear || month != prevMonth || day != prevDay) {
+ 112 | fmt.Fprintf(&recentQuestionsBuilder, "\n")
+ 113 | }
+ 114 |
+ 115 | if activity.Activity == "question" {
+ 116 | fmt.Fprintf(&recentQuestionsBuilder, "=> /~ask/%d/%d %s %s > %s (%s)\n", activity.Q.TopicId, activity.Q.Id, activity.Activity_date.Format("2006-01-02"), activity.TopicTitle, activity.Q.Title, activity.User.Username)
+ 117 | } else if activity.Activity == "answer" {
+ 118 | fmt.Fprintf(&recentQuestionsBuilder, "=> /~ask/%d/%d/a/%d %s %s > Re %s (%s)\n", activity.Q.TopicId, activity.Q.Id, activity.AnswerId, activity.Activity_date.Format("2006-01-02"), activity.TopicTitle, activity.Q.Title, activity.User.Username)
+ 119 | } else {
+ 120 | fmt.Fprintf(&recentQuestionsBuilder, "'%s'\n", activity.Activity)
+ 121 | }
+ 122 |
+ 123 | prevYear, prevMonth, prevDay = year, month, day
+ 124 | }
+ 125 |
+ 126 | request.Gemini(fmt.Sprintf(`# AuraGem Ask - Recent Activity on All Topics
+ 127 |
+ 128 | => /~ask/ AuraGem Ask Root
+ 129 |
+ 130 | %s
+ 131 | `, recentQuestionsBuilder.String()))
+ 132 | })
+ 133 |
+ 134 | s.AddRoute("/~ask/dailydigest", func(request sis.Request) {
+ 135 | dates := getRecentActivity_dates(conn)
+ 136 |
+ 137 | var builder strings.Builder
+ 138 | fmt.Fprintf(&builder, "# AuraGem Ask Daily Digest\n\n")
+ 139 | fmt.Fprintf(&builder, "=> /~ask/ AuraGem Ask Root\n")
+ 140 | query := strings.ReplaceAll(url.QueryEscape("gemini://auragem.letz.dev/~ask/dailydigest"), "%", "%%")
+ 141 | fmt.Fprintf(&builder, "=> gemini://warmedal.se/~antenna/submit?%s Update Digest on Antenna\n\n", query)
+ 142 | prevYear, prevMonth := 0, time.Month(1)
+ 143 | for _, date := range dates {
+ 144 | year, month, _ := date.Date()
+ 145 | if prevYear != 0 && (year != prevYear || month != prevMonth) {
+ 146 | fmt.Fprintf(&builder, "\n")
+ 147 | }
+ 148 | fmt.Fprintf(&builder, "=> /~ask/dailydigest/%s %s\n", date.Format("2006-01-02"), date.Format("2006-01-02"))
+ 149 |
+ 150 | prevYear, prevMonth = year, month
+ 151 | }
+ 152 |
+ 153 | request.Gemini(builder.String())
+ 154 | })
+ 155 | s.AddRoute("/~ask/dailydigest/:date", func(request sis.Request) {
+ 156 | dateString := request.GetParam("date")
+ 157 | date, err := time.Parse("2006-01-02", dateString)
+ 158 | if err != nil {
+ 159 | request.TemporaryFailure("Malformed date string.")
+ 160 | return
+ 161 | }
+ 162 |
+ 163 | var builder strings.Builder
+ 164 | activities := getRecentActivityFromDate_Questions(conn, date)
+ 165 | for _, activity := range activities {
+ 166 | if activity.Activity == "question" {
+ 167 | fmt.Fprintf(&builder, "=> /~ask/%d/%d %s %s > %s (%s)\n", activity.Q.TopicId, activity.Q.Id, activity.Activity_date.Format("2006-01-02"), activity.TopicTitle, activity.Q.Title, activity.User.Username)
+ 168 | } else if activity.Activity == "answer" {
+ 169 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d %s %s > Re %s (%s)\n", activity.Q.TopicId, activity.Q.Id, activity.AnswerId, activity.Activity_date.Format("2006-01-02"), activity.TopicTitle, activity.Q.Title, activity.User.Username)
+ 170 | } else {
+ 171 | fmt.Fprintf(&builder, "'%s'\n", activity.Activity)
+ 172 | }
+ 173 | }
+ 174 |
+ 175 | request.Gemini(fmt.Sprintf(`# %s AuraGem Ask Activity
+ 176 |
+ 177 | => /~ask/ What Is AuraGem Ask?
+ 178 | => /~ask/dailydigest Daily Digest
+ 179 |
+ 180 | %s
+ 181 | `, date.Format("2006-01-02"), builder.String()))
+ 182 | })
+ 183 |
+ 184 | s.AddRoute("/~ask/myquestions", func(request sis.Request) {
+ 185 | cert := request.UserCert
+ 186 |
+ 187 | if cert == nil {
+ 188 | request.Redirect("Please enable a certificate.")
+ 189 | return
+ 190 | } else {
+ 191 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 192 | if isRegistered {
+ 193 | getUserQuestionsPage(request, conn, user)
+ 194 | } else {
+ 195 | request.Gemini(registerNotification)
+ 196 | return
+ 197 | }
+ 198 | }
+ 199 | })
+ 200 |
+ 201 | s.AddRoute("/~ask/:topicid", func(request sis.Request) {
+ 202 | cert := request.UserCert
+ 203 |
+ 204 | if cert == nil {
+ 205 | getTopicHomepage(request, conn, (AskUser{}), false)
+ 206 | } else {
+ 207 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 208 | getTopicHomepage(request, conn, user, isRegistered)
+ 209 | }
+ 210 | })
+ 211 |
+ 212 | s.AddUploadRoute("/~ask/:topicid/create", func(request sis.Request) {
+ 213 | cert := request.UserCert
+ 214 |
+ 215 | if cert == nil {
+ 216 | getCreateQuestion(request, conn, (AskUser{}), false)
+ 217 | } else if request.Upload {
+ 218 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 219 | doCreateQuestion(request, conn, user, isRegistered)
+ 220 | }
+ 221 | })
+ 222 | s.AddRoute("/~ask/:topicid/create", func(request sis.Request) {
+ 223 | cert := request.UserCert
+ 224 |
+ 225 | if cert == nil {
+ 226 | getCreateQuestion(request, conn, (AskUser{}), false)
+ 227 | } else {
+ 228 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 229 | getCreateQuestion(request, conn, user, isRegistered)
+ 230 | }
+ 231 | })
+ 232 |
+ 233 | s.AddRoute("/~ask/:topicid/create/title", func(request sis.Request) {
+ 234 | cert := request.UserCert
+ 235 | query, err2 := request.Query()
+ 236 | if err2 != nil {
+ 237 | return
+ 238 | }
+ 239 |
+ 240 | if cert == nil {
+ 241 | getCreateQuestionTitle(request, conn, (AskUser{}), false, 0, query)
+ 242 | } else if query == "" {
+ 243 | request.RequestInput("Title of Question:")
+ 244 | return
+ 245 | } else {
+ 246 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 247 | getCreateQuestionTitle(request, conn, user, isRegistered, 0, query)
+ 248 | }
+ 249 | })
+ 250 |
+ 251 | s.AddRoute("/~ask/:topicid/create/text", func(request sis.Request) {
+ 252 | cert := request.UserCert
+ 253 | query, err2 := request.Query()
+ 254 | if err2 != nil {
+ 255 | return
+ 256 | }
+ 257 |
+ 258 | if cert == nil {
+ 259 | getCreateQuestionText(request, conn, (AskUser{}), false, 0, query)
+ 260 | } else if query == "" {
+ 261 | request.RequestInput("Text of Question:")
+ 262 | return
+ 263 | } else {
+ 264 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 265 | getCreateQuestionText(request, conn, user, isRegistered, 0, query)
+ 266 | }
+ 267 | })
+ 268 |
+ 269 | s.AddRoute("/~ask/:topicid/:questionid", func(request sis.Request) {
+ 270 | cert := request.UserCert
+ 271 |
+ 272 | if cert == nil {
+ 273 | getQuestionPage(request, conn, (AskUser{}), false)
+ 274 | } else {
+ 275 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 276 | getQuestionPage(request, conn, user, isRegistered)
+ 277 | }
+ 278 | })
+ 279 |
+ 280 | s.AddRoute("/~ask/:topicid/:questionid/addtitle", func(request sis.Request) {
+ 281 | cert := request.UserCert
+ 282 | query, err2 := request.Query()
+ 283 | if err2 != nil {
+ 284 | return
+ 285 | }
+ 286 |
+ 287 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 288 | if err != nil {
+ 289 | return
+ 290 | }
+ 291 |
+ 292 | if cert == nil {
+ 293 | getCreateQuestionTitle(request, conn, (AskUser{}), false, questionId, query)
+ 294 | } else if query == "" {
+ 295 | request.RequestInput("Title of Question:")
+ 296 | return
+ 297 | } else {
+ 298 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 299 | getCreateQuestionTitle(request, conn, user, isRegistered, questionId, query)
+ 300 | }
+ 301 | })
+ 302 |
+ 303 | s.AddRoute("/~ask/:topicid/:questionid/addtext", func(request sis.Request) {
+ 304 | cert := request.UserCert
+ 305 | query, err2 := request.Query()
+ 306 | if err2 != nil {
+ 307 | return
+ 308 | }
+ 309 |
+ 310 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 311 | if err != nil {
+ 312 | return
+ 313 | }
+ 314 |
+ 315 | if cert == nil {
+ 316 | getCreateQuestionText(request, conn, (AskUser{}), false, questionId, query)
+ 317 | } else if query == "" {
+ 318 | request.RequestInput("Text of Question:")
+ 319 | return
+ 320 | } else {
+ 321 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 322 | getCreateQuestionText(request, conn, user, isRegistered, questionId, query)
+ 323 | }
+ 324 | })
+ 325 |
+ 326 | s.AddRoute("/~ask/:topicid/:questionid/raw", func(request sis.Request) { // TODO
+ 327 | // Used for titan edits
+ 328 | cert := request.UserCert
+ 329 |
+ 330 | if cert == nil {
+ 331 | //return getQuestionPage(c, conn, (AskUser{}), false)
+ 332 | request.Redirect("Certificate required.")
+ 333 | return
+ 334 | } else {
+ 335 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 336 | if !isRegistered {
+ 337 | request.Gemini(registerNotification)
+ 338 | return
+ 339 | } else {
+ 340 | getQuestionPage(request, conn, user, isRegistered)
+ 341 | }
+ 342 | //return getQuestionPage(c, conn, user, isRegistered)
+ 343 | }
+ 344 | })
+ 345 |
+ 346 | s.AddUploadRoute("/~ask/:topicid/:questionid/a/create", func(request sis.Request) {
+ 347 | cert := request.UserCert
+ 348 |
+ 349 | if cert == nil {
+ 350 | getCreateAnswer(request, conn, (AskUser{}), false)
+ 351 | } else if request.Upload {
+ 352 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 353 | doCreateAnswer(request, conn, user, isRegistered)
+ 354 | }
+ 355 | })
+ 356 | s.AddRoute("/~ask/:topicid/:questionid/a/create", func(request sis.Request) {
+ 357 | cert := request.UserCert
+ 358 |
+ 359 | if cert == nil {
+ 360 | getCreateAnswer(request, conn, (AskUser{}), false)
+ 361 | } else {
+ 362 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 363 | getCreateAnswer(request, conn, user, isRegistered)
+ 364 | }
+ 365 | })
+ 366 |
+ 367 | s.AddRoute("/~ask/:topicid/:questionid/a/create/text", func(request sis.Request) {
+ 368 | cert := request.UserCert
+ 369 | query, err2 := request.Query()
+ 370 | if err2 != nil {
+ 371 | return
+ 372 | }
+ 373 |
+ 374 | if cert == nil {
+ 375 | getCreateAnswerText(request, conn, (AskUser{}), false, 0, query)
+ 376 | } else if query == "" {
+ 377 | request.RequestInput("Text of Answer:")
+ 378 | return
+ 379 | } else {
+ 380 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 381 | getCreateAnswerText(request, conn, user, isRegistered, 0, query)
+ 382 | }
+ 383 | })
+ 384 |
+ 385 | s.AddRoute("/~ask/:topicid/:questionid/a/create/gemlog", func(request sis.Request) {
+ 386 | cert := request.UserCert
+ 387 | query, err2 := request.Query()
+ 388 | if err2 != nil {
+ 389 | return
+ 390 | }
+ 391 |
+ 392 | if cert == nil {
+ 393 | getCreateAnswerGemlog(request, conn, (AskUser{}), false, query)
+ 394 | } else if query == "" {
+ 395 | request.RequestInput("Gemlog URL:")
+ 396 | return
+ 397 | } else {
+ 398 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 399 | getCreateAnswerGemlog(request, conn, user, isRegistered, query)
+ 400 | }
+ 401 | })
+ 402 |
+ 403 | s.AddRoute("/~ask/:topicid/:questionid/a/:answerid", func(request sis.Request) {
+ 404 | cert := request.UserCert
+ 405 |
+ 406 | if cert == nil {
+ 407 | getAnswerPage(request, conn, (AskUser{}), false)
+ 408 | } else {
+ 409 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 410 | getAnswerPage(request, conn, user, isRegistered)
+ 411 | }
+ 412 | })
+ 413 |
+ 414 | s.AddRoute("/~ask/:topicid/:questionid/a/:answerid/addtext", func(request sis.Request) {
+ 415 | cert := request.UserCert
+ 416 | query, err2 := request.Query()
+ 417 | if err2 != nil {
+ 418 | return
+ 419 | }
+ 420 |
+ 421 | answerId, err := strconv.Atoi(request.GetParam("answerid"))
+ 422 | if err != nil {
+ 423 | return
+ 424 | }
+ 425 |
+ 426 | if cert == nil {
+ 427 | getCreateAnswerText(request, conn, (AskUser{}), false, answerId, query)
+ 428 | } else if query == "" {
+ 429 | request.RequestInput("Text of Answer:")
+ 430 | return
+ 431 | } else {
+ 432 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 433 | getCreateAnswerText(request, conn, user, isRegistered, answerId, query)
+ 434 | }
+ 435 | })
+ 436 |
+ 437 | s.AddRoute("/~ask/:topicid/:questionid/a/:answerid/upvote", func(request sis.Request) {
+ 438 | cert := request.UserCert
+ 439 | query, err2 := request.Query()
+ 440 | if err2 != nil {
+ 441 | return
+ 442 | }
+ 443 | query = strings.ToLower(query)
+ 444 |
+ 445 | if cert == nil {
+ 446 | request.Redirect("Please enable a certificate.")
+ 447 | return
+ 448 | } else {
+ 449 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 450 | if !isRegistered {
+ 451 | request.Gemini(registerNotification)
+ 452 | return
+ 453 | } else if query == "" {
+ 454 | request.RequestInput("Upvote? [yes/no]")
+ 455 | return
+ 456 | } else {
+ 457 | getUpvoteAnswer(request, conn, user, query)
+ 458 | }
+ 459 | }
+ 460 | })
+ 461 |
+ 462 | s.AddRoute("/~ask/:topicid/:questionid/a/:answerid/removeupvote", func(request sis.Request) {
+ 463 | cert := request.UserCert
+ 464 | query, err2 := request.Query()
+ 465 | if err2 != nil {
+ 466 | return
+ 467 | }
+ 468 | query = strings.ToLower(query)
+ 469 |
+ 470 | if cert == nil {
+ 471 | request.Redirect("Please enable a certificate.")
+ 472 | return
+ 473 | } else {
+ 474 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
+ 475 | if !isRegistered {
+ 476 | request.Gemini(registerNotification)
+ 477 | return
+ 478 | } else if query == "" {
+ 479 | request.RequestInput("Remove Upvote? [yes/no]")
+ 480 | return
+ 481 | } else {
+ 482 | getRemoveUpvoteAnswer(request, conn, user, query)
+ 483 | }
+ 484 | }
+ 485 | })
+ 486 | }
+ 487 |
+ 488 | // -- Homepages Handling --
+ 489 |
+ 490 | func getHomepage(request sis.Request, conn *sql.DB) {
+ 491 | topics := GetTopics(conn)
+ 492 |
+ 493 | // TODO: Show user's questions
+ 494 |
+ 495 | var builder strings.Builder
+ 496 | for _, topic := range topics {
+ 497 | fmt.Fprintf(&builder, "=> /~ask/%d %s\n%s\nQuestions Asked: %d\n\n", topic.Id, topic.Title, topic.Description, topic.QuestionTotal)
+ 498 | }
+ 499 |
+ 500 | request.Gemini(fmt.Sprintf(`# AuraGem Ask
+ 501 |
+ 502 | AuraGem Ask is a Gemini-first Question and Answer service, similar to Quora and StackOverflow.
+ 503 |
+ 504 | AuraGem Ask supports uploading content via Gemini or Titan. Optionally, one can submit a URL to a gemlog that answers a particular question, and the content of the gemlog will be cached in case it goes down for any reason.
+ 505 |
+ 506 | You must register first before being able to post. Registering will simply associate a username to your certificate. To register, create and enable a client certificate and then click the link below to register your cert:
+ 507 |
+ 508 | => /~ask/?register Register Cert
+ 509 | => gemini://transjovian.org/titan About Titan
+ 510 |
+ 511 | => /~ask/dailydigest Daily Digest
+ 512 | => /~ask/recent Recent Activity on All Topics
+ 513 |
+ 514 | ## Topics
+ 515 |
+ 516 | %s
+ 517 | `, builder.String()))
+ 518 | }
+ 519 |
+ 520 | func getUserDashboard(request sis.Request, conn *sql.DB, user AskUser) {
+ 521 | topics := GetTopics(conn)
+ 522 |
+ 523 | // TODO: Show user's questions
+ 524 |
+ 525 | var builder strings.Builder
+ 526 | for _, topic := range topics {
+ 527 | fmt.Fprintf(&builder, "=> /~ask/%d %s\n%s\nQuestions Asked: %d\n\n", topic.Id, topic.Title, topic.Description, topic.QuestionTotal)
+ 528 | }
+ 529 |
+ 530 | request.Gemini(fmt.Sprintf(`# AuraGem Ask - %s
+ 531 |
+ 532 | => /~ask/dailydigest Daily Digest
+ 533 | => /~ask/recent Recent Activity on All Topics
+ 534 | => /~ask/myquestions List of Your Questions
+ 535 |
+ 536 | ## Topics
+ 537 |
+ 538 | %s
+ 539 | `, user.Username, builder.String()))
+ 540 | }
+ 541 |
+ 542 | func getUserQuestionsPage(request sis.Request, conn *sql.DB, user AskUser) {
+ 543 | var builder strings.Builder
+ 544 | userQuestions := GetUserQuestions(conn, user)
+ 545 | for _, question := range userQuestions {
+ 546 | fmt.Fprintf(&builder, "=> /~ask/%d/%d %s %s\n", question.TopicId, question.Id, question.Date_added.Format("2006-01-02"), question.Title)
+ 547 | }
+ 548 |
+ 549 | request.Gemini(fmt.Sprintf(`# AuraGem Ask - Your Questions
+ 550 |
+ 551 | => /~ask/ AuraGem Ask Root
+ 552 |
+ 553 | %s
+ 554 | `, builder.String()))
+ 555 | }
+ 556 |
+ 557 | // TODO: Pagination for all questions
+ 558 | func getTopicHomepage(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 559 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 560 | if err != nil {
+ 561 | return
+ 562 | }
+ 563 | topic, topicSuccess := getTopic(conn, topicId)
+ 564 | if !topicSuccess {
+ 565 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 566 | return
+ 567 | }
+ 568 |
+ 569 | var builder strings.Builder
+ 570 | fmt.Fprintf(&builder, "# AuraGem Ask > %s\n\n", topic.Title)
+ 571 |
+ 572 | if isRegistered {
+ 573 | fmt.Fprintf(&builder, "Welcome %s\n\n", user.Username)
+ 574 | }
+ 575 |
+ 576 | fmt.Fprintf(&builder, "=> /~ask/ AuraGem Ask Root\n")
+ 577 | if isRegistered {
+ 578 | fmt.Fprintf(&builder, "=> /~ask/%d/create Create New Question\n", topic.Id)
+ 579 | } else {
+ 580 | fmt.Fprintf(&builder, "=> /~ask/?register Register Cert\n")
+ 581 | }
+ 582 |
+ 583 | // TODOShow user's questions for this topic
+ 584 |
+ 585 | fmt.Fprintf(&builder, "\n## Recent Questions\n")
+ 586 | questions := getQuestionsForTopic(conn, topic.Id)
+ 587 | for _, question := range questions {
+ 588 | fmt.Fprintf(&builder, "=> /~ask/%d/%d %s %s (%s)\n", topic.Id, question.Id, question.Date_added.Format("2006-01-02"), question.Title, question.User.Username)
+ 589 | }
+ 590 |
+ 591 | request.Gemini(builder.String())
+ 592 | }
+ 593 |
+ 594 | // -- Question Handling --
+ 595 |
+ 596 | func getCreateQuestion(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 597 | if !isRegistered {
+ 598 | request.Gemini(registerNotification)
+ 599 | return
+ 600 | }
+ 601 |
+ 602 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 603 | if err != nil {
+ 604 | return
+ 605 | }
+ 606 | topic, topicSuccess := getTopic(conn, topicId)
+ 607 | if !topicSuccess {
+ 608 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 609 | return
+ 610 | }
+ 611 |
+ 612 | titanHost := "titan://auragem.letz.dev/"
+ 613 | if request.Hostname() == "192.168.0.60" {
+ 614 | titanHost = "titan://192.168.0.60/"
+ 615 | } else if request.Hostname() == "auragem.ddns.net" {
+ 616 | titanHost = "titan://auragem.ddns.net/"
+ 617 | }
+ 618 |
+ 619 | // /create/title?TitleHere -> Creates the question with title and blank text -> Redirects to question's new page with questionid
+ 620 | // /create/text?TextHere -> Creates the question with text and blank title -> Redirects to question's new page with questionId
+ 621 | request.Gemini(fmt.Sprintf(`# AuraGem Ask > %s - Create New Question
+ 622 |
+ 623 | => /~ask/%d %s Homepage
+ 624 |
+ 625 | To create a new question, you can do one of the following:
+ 626 |
+ 627 | 1. Upload text to this url with Titan. Use a level-1 heading ('#') for the Question Title. All other heading lines are disallowed and will be stripped. This option is suitable for long posts. Make sure your certificate/identity is selected when uploading with Titan.
+ 628 | => %s/~ask/%d/create Upload with Titan
+ 629 |
+ 630 | 2. Or add a Title and Text via Gemini with the links below. Note that Gemini limits these to 1024 bytes total.
+ 631 | => /~ask/%d/create/title Add Title
+ 632 | => /~ask/%d/create/text Add Text
+ 633 | `, topic.Title, topic.Id, topic.Title, titanHost, topic.Id, topic.Id, topic.Id))
+ 634 | }
+ 635 |
+ 636 | func doCreateQuestion(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 637 | if !isRegistered {
+ 638 | request.TemporaryFailure("You must be registered.")
+ 639 | return
+ 640 | }
+ 641 |
+ 642 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 643 | if err != nil {
+ 644 | return
+ 645 | }
+ 646 | topic, topicSuccess := getTopic(conn, topicId)
+ 647 | if !topicSuccess {
+ 648 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 649 | return
+ 650 | }
+ 651 |
+ 652 | // Check mimetype and size
+ 653 | if request.DataMime != "text/plain" && request.DataMime != "text/gemini" {
+ 654 | request.TemporaryFailure("Wrong mimetype. Only text/plain and text/gemini supported.")
+ 655 | return
+ 656 | }
+ 657 | if request.DataSize > 16*1024 {
+ 658 | request.TemporaryFailure("Size too large. Max size allowed is 16 KiB.")
+ 659 | return
+ 660 | }
+ 661 |
+ 662 | data, read_err := request.GetUploadData()
+ 663 | if read_err != nil {
+ 664 | return
+ 665 | }
+ 666 |
+ 667 | text := string(data)
+ 668 | if !utf8.ValidString(text) {
+ 669 | request.TemporaryFailure("Not a valid UTF-8 text file.")
+ 670 | return
+ 671 | }
+ 672 | if ContainsCensorWords(text) {
+ 673 | request.TemporaryFailure("Profanity or slurs were detected. Your edit is rejected.")
+ 674 | return
+ 675 | }
+ 676 |
+ 677 | strippedText, title := StripGeminiText(text)
+ 678 | question, q_err := createQuestionTitan(conn, topic.Id, title, strippedText, user)
+ 679 | if q_err != nil {
+ 680 | return
+ 681 | }
+ 682 |
+ 683 | request.Redirect(fmt.Sprintf("%s%s/~ask/%d/%d", request.Server.Scheme(), request.Hostname(), topic.Id, question.Id))
+ 684 | }
+ 685 |
+ 686 | func getQuestionPage(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 687 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 688 | if err != nil {
+ 689 | return
+ 690 | }
+ 691 | topic, topicSuccess := getTopic(conn, topicId)
+ 692 | if !topicSuccess {
+ 693 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 694 | return
+ 695 | }
+ 696 |
+ 697 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 698 | if err != nil {
+ 699 | return
+ 700 | }
+ 701 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 702 | if !questionSuccess {
+ 703 | request.TemporaryFailure("Question Id doesn't exist.")
+ 704 | return
+ 705 | }
+ 706 |
+ 707 | var builder strings.Builder
+ 708 | if question.Title != "" {
+ 709 | fmt.Fprintf(&builder, "# AuraGem Ask > %s: %s\n\n", topic.Title, question.Title)
+ 710 | } else {
+ 711 | fmt.Fprintf(&builder, "# AuraGem Ask > %s: [No Title]\n\n", topic.Title)
+ 712 | }
+ 713 | fmt.Fprintf(&builder, "=> /~ask/%d %s Homepage\n", topic.Id, topic.Title)
+ 714 | if question.Title == "" && question.MemberId == user.Id { // Only display if user owns the question
+ 715 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/addtitle Add Title\n", topic.Id, questionId)
+ 716 | } else if question.MemberId == user.Id {
+ 717 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/addtitle Edit Title\n", topic.Id, questionId)
+ 718 | }
+ 719 |
+ 720 | if question.Text != "" {
+ 721 | if question.MemberId == user.Id {
+ 722 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/addtext Edit Text\n\n", topic.Id, questionId)
+ 723 | } else {
+ 724 | fmt.Fprintf(&builder, "\n")
+ 725 | }
+ 726 | fmt.Fprintf(&builder, "%s\n\n", question.Text)
+ 727 | } else if question.MemberId == user.Id { // Only display if user owns the question
+ 728 | fmt.Fprintf(&builder, "\n")
+ 729 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/addtext Add Text\n\n", topic.Id, questionId)
+ 730 | } else {
+ 731 | fmt.Fprintf(&builder, "\n")
+ 732 | fmt.Fprintf(&builder, "[No Body Text]\n\n")
+ 733 | }
+ 734 |
+ 735 | fmt.Fprintf(&builder, "Asked %s UTC by %s\n\n", question.Date_added.Format("2006-01-02 15:04"), question.User.Username)
+ 736 | fmt.Fprintf(&builder, "## Answers List\n\n")
+ 737 |
+ 738 | if isRegistered {
+ 739 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/create Create New Answer\n\n", topic.Id, question.Id)
+ 740 | } else {
+ 741 | fmt.Fprintf(&builder, "=> /~ask/?register Register Cert\n")
+ 742 | }
+ 743 |
+ 744 | answers := getAnswersForQuestion(conn, question)
+ 745 | for _, answer := range answers {
+ 746 | // TODO: Check if gemlog answer
+ 747 | if answer.Gemlog_url != nil {
+ 748 | fmt.Fprintf(&builder, "=> %s %s Gemlog Answer by %s\n", GetNormalizedURL(answer.Gemlog_url), answer.Date_added.Format("2006-01-02"), answer.User.Username)
+ 749 | } else {
+ 750 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d %s %s\n", topic.Id, question.Id, answer.Id, answer.Date_added.Format("2006-01-02"), answer.User.Username)
+ 751 | }
+ 752 | }
+ 753 |
+ 754 | request.Gemini(builder.String())
+ 755 | }
+ 756 |
+ 757 | func getCreateQuestionTitle(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool, questionId int, title string) {
+ 758 | if !isRegistered {
+ 759 | request.Gemini(registerNotification)
+ 760 | return
+ 761 | }
+ 762 |
+ 763 | if ContainsCensorWords(title) {
+ 764 | request.TemporaryFailure("Profanity or slurs were detected. They are not allowed.")
+ 765 | return
+ 766 | }
+ 767 |
+ 768 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 769 | if err != nil {
+ 770 | return
+ 771 | }
+ 772 | topic, topicSuccess := getTopic(conn, topicId)
+ 773 | if !topicSuccess {
+ 774 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 775 | return
+ 776 | }
+ 777 |
+ 778 | // Make sure no newlines in title // TODO
+ 779 | //title = strings.Fields(title)[0]
+ 780 | title = strings.FieldsFunc(title, func(r rune) bool {
+ 781 | return r == '\n'
+ 782 | })[0]
+ 783 |
+ 784 | if questionId == 0 {
+ 785 | // Question hasn't been created yet. This is from the initial /create page. Create a new question.
+ 786 | question, q_err := createQuestionWithTitle(conn, topic.Id, title, user)
+ 787 | if q_err != nil {
+ 788 | return
+ 789 | }
+ 790 | request.Redirect(fmt.Sprintf("/~ask/%d/%d", topic.Id, question.Id))
+ 791 | return
+ 792 | } else {
+ 793 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 794 | if !questionSuccess {
+ 795 | request.TemporaryFailure("Question Id doesn't exist.")
+ 796 | return
+ 797 | }
+ 798 |
+ 799 | // Check that the current user owns this question
+ 800 | if question.MemberId != user.Id {
+ 801 | request.TemporaryFailure("You cannot edit this question, since you did not post it.")
+ 802 | return
+ 803 | }
+ 804 |
+ 805 | var q_err error
+ 806 | question, q_err = updateQuestionTitle(conn, question, title, user)
+ 807 | if q_err != nil {
+ 808 | return
+ 809 | }
+ 810 | request.Redirect(fmt.Sprintf("/~ask/%d/%d", topic.Id, question.Id))
+ 811 | return
+ 812 | }
+ 813 | }
+ 814 |
+ 815 | func getCreateQuestionText(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool, questionId int, text string) {
+ 816 | if !isRegistered {
+ 817 | request.Gemini(registerNotification)
+ 818 | return
+ 819 | }
+ 820 |
+ 821 | if ContainsCensorWords(text) {
+ 822 | request.TemporaryFailure("Profanity or slurs were detected. They are not allowed.")
+ 823 | return
+ 824 | }
+ 825 |
+ 826 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 827 | if err != nil {
+ 828 | return
+ 829 | }
+ 830 | topic, topicSuccess := getTopic(conn, topicId)
+ 831 | if !topicSuccess {
+ 832 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 833 | return
+ 834 | }
+ 835 |
+ 836 | strippedText, _ := StripGeminiText(text)
+ 837 | if questionId == 0 {
+ 838 | // Question hasn't been created yet. This is from the initial /create page. Create a new question.
+ 839 | question, q_err := createQuestionWithText(conn, topic.Id, strippedText, user)
+ 840 | if q_err != nil {
+ 841 | return
+ 842 | }
+ 843 | request.Redirect(fmt.Sprintf("/~ask/%d/%d", topic.Id, question.Id))
+ 844 | return
+ 845 | } else {
+ 846 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 847 | if !questionSuccess {
+ 848 | request.TemporaryFailure("Question Id doesn't exist.")
+ 849 | return
+ 850 | }
+ 851 |
+ 852 | // Check that the current user owns this question
+ 853 | if question.MemberId != user.Id {
+ 854 | request.TemporaryFailure("You cannot edit this question, since you did not post it.")
+ 855 | return
+ 856 | }
+ 857 |
+ 858 | var q_err error
+ 859 | question, q_err = updateQuestionText(conn, question, strippedText, user)
+ 860 | if q_err != nil {
+ 861 | return
+ 862 | }
+ 863 |
+ 864 | request.Redirect(fmt.Sprintf("/~ask/%d/%d", topic.Id, question.Id))
+ 865 | return
+ 866 | }
+ 867 | }
+ 868 |
+ 869 | // -- Answer Handling --
+ 870 |
+ 871 | func getCreateAnswer(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 872 | if !isRegistered {
+ 873 | request.Gemini(registerNotification)
+ 874 | return
+ 875 | }
+ 876 |
+ 877 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 878 | if err != nil {
+ 879 | return
+ 880 | }
+ 881 | topic, topicSuccess := getTopic(conn, topicId)
+ 882 | if !topicSuccess {
+ 883 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 884 | return
+ 885 | }
+ 886 |
+ 887 | questionid, err := strconv.Atoi(request.GetParam("questionid"))
+ 888 | if err != nil {
+ 889 | return
+ 890 | }
+ 891 | question, questionSuccess := getQuestion(conn, topic.Id, questionid)
+ 892 | if !questionSuccess {
+ 893 | request.TemporaryFailure("Question Id doesn't exist.")
+ 894 | return
+ 895 | }
+ 896 |
+ 897 | titanHost := "titan://auragem.letz.dev/"
+ 898 | if request.Hostname() == "192.168.0.60" {
+ 899 | titanHost = "titan://192.168.0.60/"
+ 900 | } else if request.Hostname() == "auragem.ddns.net" {
+ 901 | titanHost = "titan://auragem.ddns.net/"
+ 902 | }
+ 903 |
+ 904 | // /create/text?TextHere -> Creates the question with text and blank title -> Redirects to question's new page with questionId
+ 905 | request.Gemini(fmt.Sprintf(`# AuraGem Ask > %s - Create New Answer
+ 906 |
+ 907 | => /~ask/%d %s Homepage
+ 908 | => /~ask/%d/%d Back to Question
+ 909 |
+ 910 | To create a new answer, you can do one of the following:
+ 911 |
+ 912 | 1. Upload text to this url with Titan. All heading lines are disallowed and will be stripped. This option is suitable for long posts. Make sure your certificate/identity is selected when uploading with Titan.
+ 913 | => %s/~ask/%d/%d/a/create Upload with Titan
+ 914 |
+ 915 | 2. Add Text via Gemini with the link below. Note that Gemini limits these to 1024 bytes total.
+ 916 | => /~ask/%d/%d/a/create/text Add Text
+ 917 |
+ 918 | 3. Or Submit a URL to a gemlog post in response to the question
+ 919 | => /~ask/%d/%d/a/create/gemlog Post URL of Gemlog Response
+ 920 | `, topic.Title, topic.Id, topic.Title, topic.Id, question.Id, titanHost, topic.Id, question.Id, topic.Id, question.Id, topic.Id, question.Id))
+ 921 | }
+ 922 |
+ 923 | func doCreateAnswer(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 924 | if !isRegistered {
+ 925 | request.TemporaryFailure("You must be registered.")
+ 926 | return
+ 927 | }
+ 928 |
+ 929 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 930 | if err != nil {
+ 931 | return
+ 932 | }
+ 933 | topic, topicSuccess := getTopic(conn, topicId)
+ 934 | if !topicSuccess {
+ 935 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 936 | return
+ 937 | }
+ 938 |
+ 939 | questionid, err2 := strconv.Atoi(request.GetParam("questionid"))
+ 940 | if err2 != nil {
+ 941 | return
+ 942 | }
+ 943 | question, questionSuccess := getQuestion(conn, topic.Id, questionid)
+ 944 | if !questionSuccess {
+ 945 | request.TemporaryFailure("Question Id doesn't exist.")
+ 946 | return
+ 947 | }
+ 948 |
+ 949 | // Check mimetype and size
+ 950 | if request.DataMime != "text/plain" && request.DataMime != "text/gemini" {
+ 951 | request.TemporaryFailure("Wrong mimetype. Only text/plain and text/gemini supported.")
+ 952 | return
+ 953 | }
+ 954 | if request.DataSize > 16*1024 {
+ 955 | request.TemporaryFailure("Size too large. Max size allowed is 16 KiB.")
+ 956 | return
+ 957 | }
+ 958 |
+ 959 | data, read_err := request.GetUploadData()
+ 960 | if read_err != nil {
+ 961 | return
+ 962 | }
+ 963 |
+ 964 | text := string(data)
+ 965 | if !utf8.ValidString(text) {
+ 966 | request.TemporaryFailure("Not a valid UTF-8 text file.")
+ 967 | return
+ 968 | }
+ 969 | if ContainsCensorWords(text) {
+ 970 | request.TemporaryFailure("Profanity or slurs were detected. Your edit is rejected.")
+ 971 | return
+ 972 | }
+ 973 |
+ 974 | strippedText, _ := StripGeminiText(text)
+ 975 | answer, a_err := createAnswerWithText(conn, question.Id, strippedText, user)
+ 976 | if a_err != nil {
+ 977 | return
+ 978 | }
+ 979 | request.Redirect(fmt.Sprintf("%s%s/~ask/%d/%d/a/%d", request.Server.Scheme(), request.Hostname(), topic.Id, question.Id, answer.Id))
+ 980 | }
+ 981 |
+ 982 | func getCreateAnswerText(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool, answerId int, text string) {
+ 983 | if !isRegistered {
+ 984 | request.Gemini(registerNotification)
+ 985 | return
+ 986 | }
+ 987 |
+ 988 | if ContainsCensorWords(text) {
+ 989 | request.TemporaryFailure("Profanity or slurs were detected. They are not allowed.")
+ 990 | return
+ 991 | }
+ 992 |
+ 993 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 994 | if err != nil {
+ 995 | return
+ 996 | }
+ 997 | topic, topicSuccess := getTopic(conn, topicId)
+ 998 | if !topicSuccess {
+ 999 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 1000 | return
+ 1001 | }
+ 1002 |
+ 1003 | questionid, err2 := strconv.Atoi(request.GetParam("questionid"))
+ 1004 | if err2 != nil {
+ 1005 | return
+ 1006 | }
+ 1007 | question, questionSuccess := getQuestion(conn, topic.Id, questionid)
+ 1008 | if !questionSuccess {
+ 1009 | request.TemporaryFailure("Question Id doesn't exist.")
+ 1010 | return
+ 1011 | }
+ 1012 |
+ 1013 | strippedText, _ := StripGeminiText(text)
+ 1014 | if answerId == 0 {
+ 1015 | // Answer hasn't been created yet. This is from the initial /create page. Create a new question.
+ 1016 | answer, a_err := createAnswerWithText(conn, question.Id, strippedText, user)
+ 1017 | if a_err != nil {
+ 1018 | return
+ 1019 | }
+ 1020 | request.Redirect(fmt.Sprintf("/~ask/%d/%d/a/%d", topic.Id, question.Id, answer.Id))
+ 1021 | return
+ 1022 | } else {
+ 1023 | answer, answerSuccess := getAnswer(conn, question.Id, answerId)
+ 1024 | if !answerSuccess {
+ 1025 | request.TemporaryFailure("Answer Id doesn't exist.")
+ 1026 | return
+ 1027 | }
+ 1028 |
+ 1029 | // Check that the current user owns this question
+ 1030 | if answer.MemberId != user.Id {
+ 1031 | request.TemporaryFailure("You cannot edit this answer, since you did not post it.")
+ 1032 | return
+ 1033 | }
+ 1034 |
+ 1035 | var a_err error
+ 1036 | answer, a_err = updateAnswerText(conn, answer, strippedText, user)
+ 1037 | if a_err != nil {
+ 1038 | return
+ 1039 | }
+ 1040 |
+ 1041 | request.Redirect(fmt.Sprintf("/~ask/%d/%d/a/%d", topic.Id, question.Id, answer.Id))
+ 1042 | return
+ 1043 | }
+ 1044 | }
+ 1045 |
+ 1046 | func getCreateAnswerGemlog(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool, gemlogUrl string) {
+ 1047 | if !isRegistered {
+ 1048 | request.Gemini(registerNotification)
+ 1049 | return
+ 1050 | }
+ 1051 |
+ 1052 | /*if ContainsCensorWords(text) {
+ 1053 | request.TemporaryFailure("Profanity or slurs were detected. They are not allowed.")
+ 1054 | return
+ 1055 | }*/
+ 1056 |
+ 1057 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 1058 | if err != nil {
+ 1059 | return
+ 1060 | }
+ 1061 | topic, topicSuccess := getTopic(conn, topicId)
+ 1062 | if !topicSuccess {
+ 1063 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 1064 | return
+ 1065 | }
+ 1066 |
+ 1067 | questionid, err2 := strconv.Atoi(request.GetParam("questionid"))
+ 1068 | if err2 != nil {
+ 1069 | return
+ 1070 | }
+ 1071 | question, questionSuccess := getQuestion(conn, topic.Id, questionid)
+ 1072 | if !questionSuccess {
+ 1073 | request.TemporaryFailure("Question Id doesn't exist.")
+ 1074 | return
+ 1075 | }
+ 1076 |
+ 1077 | gemlogUrlNormalized, url_err := checkValidUrl(gemlogUrl)
+ 1078 | if url_err != nil {
+ 1079 | return
+ 1080 | }
+ 1081 |
+ 1082 | // Get text of gemlog so we can cache it in the DB
+ 1083 | client := gemini2.DefaultClient
+ 1084 | resp, fetch_err := client.Fetch(gemlogUrlNormalized)
+ 1085 | if fetch_err != nil {
+ 1086 | request.TemporaryFailure("Failed to fetch gemlog at given url.")
+ 1087 | return
+ 1088 | } else if resp.Status == 30 || resp.Status == 31 {
+ 1089 | request.TemporaryFailure("Failed to fetch gemlog at given url. Links that redirect are not allowed.")
+ 1090 | return
+ 1091 | } else if resp.Status != 20 {
+ 1092 | request.TemporaryFailure("Failed to fetch gemlog at given url.")
+ 1093 | return
+ 1094 | }
+ 1095 |
+ 1096 | _, a_err := createAnswerAsGemlog(conn, question.Id, gemlogUrlNormalized, user)
+ 1097 | if a_err != nil {
+ 1098 | return
+ 1099 | }
+ 1100 | request.Redirect(fmt.Sprintf("/~ask/%d/%d", topic.Id, question.Id))
+ 1101 | }
+ 1102 |
+ 1103 | func getAnswerPage(request sis.Request, conn *sql.DB, user AskUser, isRegistered bool) {
+ 1104 | // Get Topic
+ 1105 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 1106 | if err != nil {
+ 1107 | return
+ 1108 | }
+ 1109 | topic, topicSuccess := getTopic(conn, topicId)
+ 1110 | if !topicSuccess {
+ 1111 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 1112 | return
+ 1113 | }
+ 1114 |
+ 1115 | // Get Question
+ 1116 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 1117 | if err != nil {
+ 1118 | return
+ 1119 | }
+ 1120 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 1121 | if !questionSuccess {
+ 1122 | request.TemporaryFailure("Question Id doesn't exist.")
+ 1123 | return
+ 1124 | }
+ 1125 |
+ 1126 | // Get Answer
+ 1127 | answerId, a_err := strconv.Atoi(request.GetParam("answerid"))
+ 1128 | if a_err != nil {
+ 1129 | return
+ 1130 | }
+ 1131 | answer, answerSuccess := getAnswer(conn, question.Id, answerId)
+ 1132 | if !answerSuccess {
+ 1133 | request.TemporaryFailure("Answer Id doesn't exist.")
+ 1134 | return
+ 1135 | }
+ 1136 |
+ 1137 | // Get Upvotes
+ 1138 | upvotes := getUpvotesWithUsers(conn, answer)
+ 1139 | upvotesCount := len(upvotes)
+ 1140 | currentUserHasUpvoted := false
+ 1141 | for _, upvote := range upvotes {
+ 1142 | if upvote.MemberId == user.Id {
+ 1143 | currentUserHasUpvoted = true
+ 1144 | break
+ 1145 | }
+ 1146 | }
+ 1147 |
+ 1148 | var builder strings.Builder
+ 1149 | if question.Title != "" {
+ 1150 | fmt.Fprintf(&builder, "# AuraGem Ask > %s: Re %s\n\n", topic.Title, question.Title)
+ 1151 | } else {
+ 1152 | fmt.Fprintf(&builder, "# AuraGem Ask > %s: Re [No Title]\n\n", topic.Title)
+ 1153 | }
+ 1154 |
+ 1155 | fmt.Fprintf(&builder, "=> /~ask/%d/%d Back to the Question\n", topic.Id, question.Id)
+ 1156 |
+ 1157 | if answer.Text != "" {
+ 1158 | if answer.MemberId == user.Id && len(answer.Text) < 1024 { // Don't show Gemini Edit if text is under 1024 bytes
+ 1159 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d/addtext Edit Text\n\n", topic.Id, question.Id, answer.Id)
+ 1160 | } else {
+ 1161 | fmt.Fprintf(&builder, "\n")
+ 1162 | }
+ 1163 | fmt.Fprintf(&builder, "%s\n\n", answer.Text)
+ 1164 | } else if answer.MemberId == user.Id && answer.Gemlog_url == nil { // Only display if user owns the question
+ 1165 | fmt.Fprintf(&builder, "\n")
+ 1166 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d/addtext Add Text\n\n", topic.Id, question.Id, answer.Id)
+ 1167 | } else if answer.Gemlog_url != nil {
+ 1168 | fmt.Fprintf(&builder, "\n")
+ 1169 | fmt.Fprintf(&builder, "=> %s Link to Gemlog\n", GetNormalizedURL(answer.Gemlog_url))
+ 1170 | } else {
+ 1171 | fmt.Fprintf(&builder, "\n")
+ 1172 | fmt.Fprintf(&builder, "[No Body Text]\n\n")
+ 1173 | }
+ 1174 |
+ 1175 | fmt.Fprintf(&builder, "Answered %s UTC by %s\n", answer.Date_added.Format("2006-01-02 15:04"), answer.User.Username)
+ 1176 | fmt.Fprintf(&builder, "Upvotes: %d\n\n", upvotesCount)
+ 1177 |
+ 1178 | if isRegistered {
+ 1179 | if currentUserHasUpvoted {
+ 1180 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d/removeupvote Remove Upvote\n", topic.Id, question.Id, answer.Id)
+ 1181 | } else {
+ 1182 | fmt.Fprintf(&builder, "=> /~ask/%d/%d/a/%d/upvote Upvote\n", topic.Id, question.Id, answer.Id)
+ 1183 | }
+ 1184 | } else {
+ 1185 | fmt.Fprintf(&builder, "=> /~ask/?register Register Cert\n")
+ 1186 | }
+ 1187 | /*fmt.Fprintf(&builder, "## Comments\n\n")*/
+ 1188 |
+ 1189 | request.Gemini(builder.String())
+ 1190 | }
+ 1191 |
+ 1192 | func getUpvoteAnswer(request sis.Request, conn *sql.DB, user AskUser, query string) {
+ 1193 | // Get Topic
+ 1194 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 1195 | if err != nil {
+ 1196 | return
+ 1197 | }
+ 1198 | topic, topicSuccess := getTopic(conn, topicId)
+ 1199 | if !topicSuccess {
+ 1200 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 1201 | return
+ 1202 | }
+ 1203 |
+ 1204 | // Get Question
+ 1205 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 1206 | if err != nil {
+ 1207 | return
+ 1208 | }
+ 1209 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 1210 | if !questionSuccess {
+ 1211 | request.TemporaryFailure("Question Id doesn't exist.")
+ 1212 | return
+ 1213 | }
+ 1214 |
+ 1215 | // Get Answer
+ 1216 | answerId, a_err := strconv.Atoi(request.GetParam("answerid"))
+ 1217 | if a_err != nil {
+ 1218 | return
+ 1219 | }
+ 1220 | answer, answerSuccess := getAnswer(conn, question.Id, answerId)
+ 1221 | if !answerSuccess {
+ 1222 | request.TemporaryFailure("Answer Id doesn't exist.")
+ 1223 | return
+ 1224 | }
+ 1225 |
+ 1226 | if query == "yes" || query == "y" {
+ 1227 | _, db_err := addUpvote(conn, answer, user)
+ 1228 | if db_err != nil {
+ 1229 | return
+ 1230 | }
+ 1231 | }
+ 1232 |
+ 1233 | request.Redirect("/~ask/%d/%d/a/%d", topic.Id, question.Id, answer.Id)
+ 1234 | }
+ 1235 |
+ 1236 | func getRemoveUpvoteAnswer(request sis.Request, conn *sql.DB, user AskUser, query string) {
+ 1237 | // Get Topic
+ 1238 | topicId, err := strconv.Atoi(request.GetParam("topicid"))
+ 1239 | if err != nil {
+ 1240 | return
+ 1241 | }
+ 1242 | topic, topicSuccess := getTopic(conn, topicId)
+ 1243 | if !topicSuccess {
+ 1244 | request.TemporaryFailure("Topic Id doesn't exist.")
+ 1245 | return
+ 1246 | }
+ 1247 |
+ 1248 | // Get Question
+ 1249 | questionId, err := strconv.Atoi(request.GetParam("questionid"))
+ 1250 | if err != nil {
+ 1251 | return
+ 1252 | }
+ 1253 | question, questionSuccess := getQuestion(conn, topic.Id, questionId)
+ 1254 | if !questionSuccess {
+ 1255 | request.TemporaryFailure("Question Id doesn't exist.")
+ 1256 | return
+ 1257 | }
+ 1258 |
+ 1259 | // Get Answer
+ 1260 | answerId, a_err := strconv.Atoi(request.GetParam("answerid"))
+ 1261 | if a_err != nil {
+ 1262 | return
+ 1263 | }
+ 1264 | answer, answerSuccess := getAnswer(conn, question.Id, answerId)
+ 1265 | if !answerSuccess {
+ 1266 | request.TemporaryFailure("Answer Id doesn't exist.")
+ 1267 | return
+ 1268 | }
+ 1269 |
+ 1270 | if query == "yes" || query == "y" {
+ 1271 | db_err := removeUpvote(conn, answer, user)
+ 1272 | if db_err != nil {
+ 1273 | return
+ 1274 | }
+ 1275 | }
+ 1276 |
+ 1277 | request.Redirect("/~ask/%d/%d/a/%d", topic.Id, question.Id, answer.Id)
+ 1278 | }
+ 1279 |
+ 1280 | func CensorWords(str string) string {
+ 1281 | wordCensors := []string{"fuck", "kill", "die", "damn", "ass", "shit", "stupid", "faggot", "fag", "whore", "cock", "cunt", "motherfucker", "fucker", "asshole", "nigger", "abbie", "abe", "abie", "abid", "abeed", "ape", "armo", "nazi", "ashke-nazi", "אשכנאצי", "bamboula", "barbarian", "beaney", "beaner", "bohunk", "boerehater", "boer-hater", "burrhead", "burr-head", "chode", "chad", "penis", "vagina", "porn", "bbc", "stealthing", "bbw", "Hentai", "milf", "dilf", "tummysticks", "heeb", "hymie", "kike", "jidan", "sheeny", "shylock", "zhyd", "yid", "shyster", "smouch"}
+ 1282 |
+ 1283 | var result string = str
+ 1284 | for _, forbiddenWord := range wordCensors {
+ 1285 | replacement := strings.Repeat("*", len(forbiddenWord))
+ 1286 | result = strings.Replace(result, forbiddenWord, replacement, -1)
+ 1287 | }
+ 1288 |
+ 1289 | return result
+ 1290 | }
+ 1291 |
+ 1292 | func ContainsCensorWords(str string) bool {
+ 1293 | wordCensors := map[string]bool{"fuck": true, "f*ck": true, "kill": true, "k*ll": true, "die": true, "damn": true, "ass": true, "*ss": true, "shit": true, "sh*t": true, "stupid": true, "faggot": true, "fag": true, "f*g": true, "whore": true, "wh*re": true, "cock": true, "c*ck": true, "cunt": true, "c*nt": true, "motherfucker": true, "fucker": true, "f*cker": true, "asshole": true, "*sshole": true, "nigger": true, "n*gger": true, "n*gg*r": true, "abbie": true, "abe": true, "abie": true, "abid": true, "abeed": true, "ape": true, "armo": true, "nazi": true, "ashke-nazi": true, "אשכנאצי": true, "bamboula": true, "barbarian": true, "beaney": true, "beaner": true, "bohunk": true, "boerehater": true, "boer-hater": true, "burrhead": true, "burr-head": true, "chode": true, "chad": true, "penis": true, "vagina": true, "porn": true, "stealthing": true, "bbw": true, "Hentai": true, "milf": true, "dilf": true, "tummysticks": true, "heeb": true, "hymie": true, "kike": true, "k*ke": true, "jidan": true, "sheeny": true, "shylock": true, "zhyd": true, "yid": true, "shyster": true, "smouch": true}
+ 1294 |
+ 1295 | fields := strings.FieldsFunc(strings.ToLower(str), func(r rune) bool {
+ 1296 | if r == '*' {
+ 1297 | return false
+ 1298 | }
+ 1299 | return unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r) || unicode.IsDigit(r) || !unicode.IsPrint(r)
+ 1300 | })
+ 1301 |
+ 1302 | for _, word := range fields {
+ 1303 | if _, ok := wordCensors[word]; ok {
+ 1304 | return true
+ 1305 | }
+ 1306 | }
+ 1307 |
+ 1308 | return false
+ 1309 | }
+ 1310 |
+ 1311 | var InvalidURLString = errors.New("URL is not a valid UTF-8 string.")
+ 1312 | var URLTooLong = errors.New("URL exceeds 1024 bytes.")
+ 1313 | var InvalidURL = errors.New("URL is not valid.")
+ 1314 | var URLRelative = errors.New("URL is relative. Only absolute URLs can be added.")
+ 1315 | var URLNotGemini = errors.New("Must be a Gemini URL.")
+ 1316 |
+ 1317 | func checkValidUrl(s string) (string, error) {
+ 1318 | // Make sure URL is a valid UTF-8 string
+ 1319 | if !utf8.ValidString(s) {
+ 1320 | return "", InvalidURLString
+ 1321 | }
+ 1322 | // Make sure URL doesn't exceed 1024 bytes
+ 1323 | if len(s) > 1024 {
+ 1324 | return "", URLTooLong
+ 1325 | }
+ 1326 | // Make sure URL has gemini:// scheme
+ 1327 | if !strings.HasPrefix(s, "gemini://") && !strings.Contains(s, "://") && !strings.HasPrefix(s, ".") && !strings.HasPrefix(s, "/") {
+ 1328 | s = "gemini://" + s
+ 1329 | }
+ 1330 |
+ 1331 | // Make sure the url is parseable and that only the hostname is being added
+ 1332 | u, urlErr := url.Parse(s)
+ 1333 | if urlErr != nil { // Check if able to parse
+ 1334 | return "", InvalidURL
+ 1335 | }
+ 1336 | if !u.IsAbs() { // Check if Absolute URL
+ 1337 | return "", URLRelative
+ 1338 | }
+ 1339 | if u.Scheme != "gemini" { // Make sure scheme is gemini
+ 1340 | return "", URLNotGemini
+ 1341 | }
+ 1342 |
+ 1343 | return GetNormalizedURL(u), nil
+ 1344 | }
+ 1345 |
+ 1346 | func GetNormalizedURL(u *url.URL) string {
+ 1347 | var buf strings.Builder
+ 1348 |
+ 1349 | // Hostname
+ 1350 | if u.Port() == "" || u.Port() == "1965" {
+ 1351 | buf.WriteString(u.Scheme)
+ 1352 | buf.WriteString("://")
+ 1353 | buf.WriteString(u.Hostname())
+ 1354 | //buf.WriteString("/")
+ 1355 | } else {
+ 1356 | buf.WriteString(u.Scheme)
+ 1357 | buf.WriteString("://")
+ 1358 | buf.WriteString(u.Hostname())
+ 1359 | buf.WriteByte(':')
+ 1360 | buf.WriteString(u.Port())
+ 1361 | //buf.WriteString("/")
+ 1362 | }
+ 1363 |
+ 1364 | // Path
+ 1365 | path := u.EscapedPath()
+ 1366 | if path == "" || (path != "" && path[0] != '/' && u.Host != "") {
+ 1367 | buf.WriteByte('/')
+ 1368 | }
+ 1369 | buf.WriteString(path)
+ 1370 |
+ 1371 | // Queries and Fragments
+ 1372 | if u.ForceQuery || u.RawQuery != "" {
+ 1373 | buf.WriteByte('?')
+ 1374 | buf.WriteString(u.RawQuery)
+ 1375 | }
+ 1376 | if u.Fragment != "" {
+ 1377 | buf.WriteByte('#')
+ 1378 | buf.WriteString(u.EscapedFragment())
+ 1379 | }
+ 1380 |
+ 1381 | return buf.String()
+ 1382 | }
+ 1383 |
+ 1384 | // Go through gemini document to get keywords, title, and links
+ 1385 | func StripGeminiText(s string) (string, string) {
+ 1386 | var strippedTextBuilder strings.Builder
+ 1387 | text, _ := gemini.ParseText(strings.NewReader(s))
+ 1388 | title := ""
+ 1389 |
+ 1390 | for _, line := range text {
+ 1391 | switch v := line.(type) {
+ 1392 | case gemini.LineHeading1:
+ 1393 | {
+ 1394 | if title == "" {
+ 1395 | title = string(v)
+ 1396 | }
+ 1397 | }
+ 1398 | case gemini.LineHeading2:
+ 1399 | {
+ 1400 | }
+ 1401 | case gemini.LineHeading3:
+ 1402 | {
+ 1403 | }
+ 1404 | case gemini.LineLink:
+ 1405 | {
+ 1406 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1407 | }
+ 1408 | case gemini.LineListItem:
+ 1409 | {
+ 1410 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1411 | }
+ 1412 | case gemini.LinePreformattingToggle:
+ 1413 | {
+ 1414 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1415 | }
+ 1416 | case gemini.LinePreformattedText:
+ 1417 | {
+ 1418 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1419 | }
+ 1420 | case gemini.LineQuote:
+ 1421 | {
+ 1422 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1423 | }
+ 1424 | case gemini.LineText:
+ 1425 | {
+ 1426 | fmt.Fprintf(&strippedTextBuilder, "%s\n", v.String())
+ 1427 | }
+ 1428 | }
+ 1429 | }
+ 1430 |
+ 1431 | // TODO: Strip blank lines at beginning and end of string
+ 1432 | // Use strings.TrimSpace?
+ 1433 |
+ 1434 | return strings.TrimSpace(strippedTextBuilder.String()), title
+ 1435 | }
gemini/ask/db.go (created)
+ 1 | package ask
+ 2 |
+ 3 | import (
+ 4 | "context"
+ 5 | "database/sql"
+ 6 | "strings"
+ 7 | "time"
+ 8 |
+ 9 | sis "gitlab.com/clseibold/smallnetinformationservices"
+ 10 | )
+ 11 |
+ 12 | func GetUser(conn *sql.DB, certHash string) (AskUser, bool) {
+ 13 | //query := "SELECT id, username, language, timezone, is_staff, is_active, date_joined FROM members LEFT JOIN membercerts ON membercerts.memberid = members.id WHERE membercerts.certificate=?"
+ 14 | query := "SELECT membercerts.id, membercerts.memberid, membercerts.title, membercerts.certificate, membercerts.is_active, membercerts.date_added, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM membercerts LEFT JOIN members ON membercerts.memberid = members.id WHERE membercerts.certificate=? AND membercerts.is_active = true"
+ 15 | row := conn.QueryRowContext(context.Background(), query, certHash)
+ 16 |
+ 17 | var user AskUser
+ 18 | var certTitle interface{}
+ 19 | err := row.Scan(&user.Certificate.Id, &user.Certificate.MemberId, &certTitle, &user.Certificate.Certificate, &user.Certificate.Is_active, &user.Certificate.Date_added, &user.Id, &user.Username, &user.Language, &user.Timezone, &user.Is_staff, &user.Is_active, &user.Date_joined)
+ 20 | if err == sql.ErrNoRows {
+ 21 | return AskUser{}, false
+ 22 | } else if err != nil {
+ 23 | //panic(err)
+ 24 | return AskUser{}, false
+ 25 | }
+ 26 | if certTitle != nil {
+ 27 | user.Certificate.Title = certTitle.(string)
+ 28 | }
+ 29 |
+ 30 | return user, true
+ 31 | }
+ 32 |
+ 33 | func RegisterUser(request sis.Request, conn *sql.DB, username string, certHash string) {
+ 34 | username = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(username, "register"), "?"), "+"))
+ 35 | // Ensure user doesn't already exist
+ 36 | row := conn.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM membercerts WHERE certificate=?", certHash)
+ 37 |
+ 38 | var numRows int
+ 39 | err := row.Scan(&numRows)
+ 40 | if err != nil {
+ 41 | panic(err)
+ 42 | }
+ 43 | if numRows < 1 {
+ 44 | // Certificate doesn't already exist - Register User by adding the member first, then the certificate after getting the memberid
+ 45 | zone, _ := time.Now().Zone()
+ 46 |
+ 47 | // TODO: Handle row2.Error and scan error
+ 48 | var user AskUser
+ 49 | row2 := conn.QueryRowContext(context.Background(), "INSERT INTO members (username, language, timezone, is_staff, is_active, date_joined) VALUES (?, ?, ?, ?, ?, ?) returning id, username, language, timezone, is_staff, is_active, date_joined", username, "en-US", zone, false, true, time.Now())
+ 50 | row2.Scan(&user.Id, &user.Username, &user.Language, &user.Timezone, &user.Is_staff, &user.Is_active, &user.Date_joined)
+ 51 |
+ 52 | // TODO: Handle row3.Error and scan error
+ 53 | var cert AskUserCert
+ 54 | row3 := conn.QueryRowContext(context.Background(), "INSERT INTO membercerts (memberid, certificate, is_active, date_added) VALUES (?, ?, ?, ?) returning id, memberid, title, certificate, is_active, date_added", user.Id, certHash, true, time.Now())
+ 55 | row3.Scan(&cert.Id, &cert.MemberId, &cert.Title, &cert.Is_active, &cert.Date_added)
+ 56 | user.Certificate = cert
+ 57 | }
+ 58 |
+ 59 | request.Redirect("/~ask/")
+ 60 | }
+ 61 |
+ 62 | func GetUserQuestions(conn *sql.DB, user AskUser) []Question {
+ 63 | query := "SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, questions.memberid, questions.date_added FROM questions WHERE questions.memberid=? ORDER BY questions.date_added DESC"
+ 64 | rows, rows_err := conn.QueryContext(context.Background(), query, user.Id)
+ 65 |
+ 66 | var questions []Question
+ 67 | if rows_err == nil {
+ 68 | defer rows.Close()
+ 69 | for rows.Next() {
+ 70 | question, scan_err := scanQuestionRows(rows)
+ 71 | if scan_err == nil {
+ 72 | question.User = user
+ 73 | questions = append(questions, question)
+ 74 | } else {
+ 75 | panic(scan_err)
+ 76 | }
+ 77 | }
+ 78 | }
+ 79 |
+ 80 | return questions
+ 81 | }
+ 82 |
+ 83 | // With Totals
+ 84 | func GetTopics(conn *sql.DB) []Topic {
+ 85 | query := "SELECT topics.ID, topics.title, topics.description, topics.date_added, COUNT(questions.id) FROM topics LEFT JOIN questions ON questions.topicid=topics.id GROUP BY topics.ID, topics.title, topics.description, topics.date_added ORDER BY topics.ID"
+ 86 | rows, rows_err := conn.QueryContext(context.Background(), query)
+ 87 |
+ 88 | var topics []Topic
+ 89 | if rows_err == nil {
+ 90 | defer rows.Close()
+ 91 | for rows.Next() {
+ 92 | var topic Topic
+ 93 | scan_err := rows.Scan(&topic.Id, &topic.Title, &topic.Description, &topic.Date_added, &topic.QuestionTotal)
+ 94 | if scan_err == nil {
+ 95 | topics = append(topics, topic)
+ 96 | } else {
+ 97 | panic(scan_err)
+ 98 | }
+ 99 | }
+ 100 | }
+ 101 |
+ 102 | return topics
+ 103 | }
+ 104 |
+ 105 | func getTopic(conn *sql.DB, topicid int) (Topic, bool) {
+ 106 | query := "SELECT * FROM topics WHERE id=?"
+ 107 | row := conn.QueryRowContext(context.Background(), query, topicid)
+ 108 |
+ 109 | var topic Topic
+ 110 | err := row.Scan(&topic.Id, &topic.Title, &topic.Description, &topic.Date_added)
+ 111 | if err == sql.ErrNoRows {
+ 112 | return Topic{}, false
+ 113 | } else if err != nil {
+ 114 | return Topic{}, false
+ 115 | }
+ 116 |
+ 117 | return topic, true
+ 118 | }
+ 119 |
+ 120 | func getRecentActivity_dates(conn *sql.DB) []time.Time {
+ 121 | query := `SELECT DISTINCT cast(a.activity_date as date)
+ 122 | FROM (SELECT questions.date_added as activity_date FROM questions
+ 123 | UNION ALL
+ 124 | SELECT answers.date_added as activity_date FROM answers LEFT JOIN questions ON questions.id=answers.questionid) a
+ 125 | ORDER BY a.activity_date DESC`
+ 126 |
+ 127 | rows, rows_err := conn.QueryContext(context.Background(), query)
+ 128 |
+ 129 | var times []time.Time
+ 130 | if rows_err == nil {
+ 131 | defer rows.Close()
+ 132 | for rows.Next() {
+ 133 | var t time.Time
+ 134 | scan_err := rows.Scan(&t)
+ 135 | if scan_err == nil {
+ 136 | times = append(times, t)
+ 137 | } else {
+ 138 | panic(scan_err)
+ 139 | }
+ 140 | }
+ 141 | }
+ 142 |
+ 143 | return times
+ 144 | }
+ 145 |
+ 146 | func getRecentActivity_Questions(conn *sql.DB) []Activity {
+ 147 | query := `SELECT a.id, a.topicid, a.title, a.text, a.tags, a.memberid, a.activity, a.activity_date, a.answerid, members.*, topics.title
+ 148 | FROM (SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, questions.memberid, 'question' as activity, questions.date_added as activity_date, 0 as answerid FROM questions
+ 149 | UNION ALL
+ 150 | SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, answers.memberid, 'answer' as activity, answers.date_added as activity_date, answers.id as answerid FROM answers LEFT JOIN questions ON questions.id=answers.questionid) a
+ 151 | LEFT JOIN members ON members.id=a.memberid
+ 152 | LEFT JOIN topics ON a.topicid=topics.id
+ 153 | ORDER BY a.activity_date DESC`
+ 154 |
+ 155 | rows, rows_err := conn.QueryContext(context.Background(), query)
+ 156 |
+ 157 | var activities []Activity
+ 158 | if rows_err == nil {
+ 159 | defer rows.Close()
+ 160 | for rows.Next() {
+ 161 | activity, scan_err := scanActivityWithUser(rows)
+ 162 | if scan_err == nil {
+ 163 | activities = append(activities, activity)
+ 164 | } else {
+ 165 | panic(scan_err)
+ 166 | }
+ 167 | }
+ 168 | }
+ 169 |
+ 170 | return activities
+ 171 | }
+ 172 |
+ 173 | func getRecentActivityFromDate_Questions(conn *sql.DB, date time.Time) []Activity {
+ 174 | query := `SELECT a.id, a.topicid, a.title, a.text, a.tags, a.memberid, a.activity, a.activity_date, a.answerid, members.*, topics.title
+ 175 | FROM (SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, questions.memberid, 'question' as activity, questions.date_added as activity_date, 0 as answerid FROM questions
+ 176 | UNION ALL
+ 177 | SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, answers.memberid, 'answer' as activity, answers.date_added as activity_date, answers.id as answerid FROM answers LEFT JOIN questions ON questions.id=answers.questionid) a
+ 178 | LEFT JOIN members ON members.id=a.memberid
+ 179 | LEFT JOIN topics ON a.topicid=topics.id
+ 180 | WHERE cast(a.activity_date as date) = ?
+ 181 | ORDER BY a.activity_date DESC`
+ 182 |
+ 183 | rows, rows_err := conn.QueryContext(context.Background(), query, date)
+ 184 |
+ 185 | var activities []Activity
+ 186 | if rows_err == nil {
+ 187 | defer rows.Close()
+ 188 | for rows.Next() {
+ 189 | activity, scan_err := scanActivityWithUser(rows)
+ 190 | if scan_err == nil {
+ 191 | activities = append(activities, activity)
+ 192 | } else {
+ 193 | panic(scan_err)
+ 194 | }
+ 195 | }
+ 196 | }
+ 197 |
+ 198 | return activities
+ 199 | }
+ 200 |
+ 201 | func getQuestionsForTopic(conn *sql.DB, topicid int) []Question {
+ 202 | query := "SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, questions.memberid, questions.date_added, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM questions LEFT JOIN members ON members.id=questions.memberid WHERE questions.topicid=? ORDER BY questions.date_added DESC"
+ 203 | rows, rows_err := conn.QueryContext(context.Background(), query, topicid)
+ 204 |
+ 205 | var questions []Question
+ 206 | if rows_err == nil {
+ 207 | defer rows.Close()
+ 208 | for rows.Next() {
+ 209 | question, scan_err := scanQuestionRowsWithUser(rows)
+ 210 | if scan_err == nil {
+ 211 | questions = append(questions, question)
+ 212 | } else {
+ 213 | panic(scan_err)
+ 214 | }
+ 215 | }
+ 216 | }
+ 217 |
+ 218 | return questions
+ 219 | }
+ 220 |
+ 221 | func getQuestion(conn *sql.DB, topicid int, questionid int) (Question, bool) {
+ 222 | // TODO: Get Selected Answer as well
+ 223 | query := "SELECT questions.id, questions.topicid, questions.title, questions.text, questions.tags, questions.memberid, questions.date_added, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM questions LEFT JOIN members ON members.id=questions.memberid WHERE questions.id=? AND questions.topicid=?"
+ 224 | row := conn.QueryRowContext(context.Background(), query, questionid, topicid)
+ 225 |
+ 226 | question, err := scanQuestionWithUser(row)
+ 227 | if err == sql.ErrNoRows {
+ 228 | return Question{}, false
+ 229 | } else if err != nil {
+ 230 | return Question{}, false
+ 231 | }
+ 232 |
+ 233 | return question, true
+ 234 | }
+ 235 |
+ 236 | func createQuestionWithTitle(conn *sql.DB, topicid int, title string, user AskUser) (Question, error) {
+ 237 | query := "INSERT INTO questions (topicid, title, memberid, date_added) VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, topicid, title, text, tags, memberid, date_added"
+ 238 | row := conn.QueryRowContext(context.Background(), query, topicid, title, user.Id)
+ 239 | return scanQuestion(row)
+ 240 | }
+ 241 | func createQuestionWithText(conn *sql.DB, topicid int, text string, user AskUser) (Question, error) {
+ 242 | query := "INSERT INTO questions (topicid, text, memberid, date_added) VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, topicid, title, text, tags, memberid, date_added"
+ 243 | row := conn.QueryRowContext(context.Background(), query, topicid, text, user.Id)
+ 244 | return scanQuestion(row)
+ 245 | }
+ 246 | func createQuestionTitan(conn *sql.DB, topicid int, title string, text string, user AskUser) (Question, error) {
+ 247 | query := "INSERT INTO questions (topicid, title, text, memberid, date_added) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, topicid, title, text, tags, memberid, date_added"
+ 248 | row := conn.QueryRowContext(context.Background(), query, topicid, title, text, user.Id)
+ 249 | return scanQuestion(row)
+ 250 | }
+ 251 |
+ 252 | func updateQuestionTitle(conn *sql.DB, question Question, title string, user AskUser) (Question, error) {
+ 253 | query := "UPDATE questions SET title=? WHERE id=? AND memberid=? RETURNING id, topicid, title, text, tags, memberid, date_added"
+ 254 | row := conn.QueryRowContext(context.Background(), query, title, question.Id, user.Id)
+ 255 | return scanQuestion(row)
+ 256 | }
+ 257 | func updateQuestionText(conn *sql.DB, question Question, text string, user AskUser) (Question, error) {
+ 258 | query := "UPDATE questions SET text=? WHERE id=? AND memberid=? RETURNING id, topicid, title, text, tags, memberid, date_added"
+ 259 | row := conn.QueryRowContext(context.Background(), query, text, question.Id, user.Id)
+ 260 | return scanQuestion(row)
+ 261 | }
+ 262 |
+ 263 | func getAnswersForQuestion(conn *sql.DB, question Question) []Answer {
+ 264 | query := "SELECT answers.*, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM answers LEFT JOIN members ON answers.memberid=members.id WHERE answers.questionid=? ORDER BY date_added ASC"
+ 265 | rows, rows_err := conn.QueryContext(context.Background(), query, question.Id)
+ 266 |
+ 267 | var answers []Answer
+ 268 | if rows_err == nil {
+ 269 | defer rows.Close()
+ 270 | for rows.Next() {
+ 271 | answer, scan_err := scanAnswerRows(rows)
+ 272 | if scan_err == nil {
+ 273 | answers = append(answers, answer)
+ 274 | } else {
+ 275 | panic(scan_err)
+ 276 | }
+ 277 | }
+ 278 | }
+ 279 |
+ 280 | return answers
+ 281 | }
+ 282 |
+ 283 | func getAnswer(conn *sql.DB, questionid int, answerid int) (Answer, bool) {
+ 284 | // TODO: Get Selected Answer as well
+ 285 | query := "SELECT answers.id, answers.questionid, answers.text, answers.gemlog_url, answers.memberid, answers.date_added, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM answers LEFT JOIN members ON members.id=answers.memberid WHERE answers.id=? AND answers.questionid=?"
+ 286 | row := conn.QueryRowContext(context.Background(), query, answerid, questionid)
+ 287 |
+ 288 | answer, err := scanAnswerWithUser(row)
+ 289 | if err == sql.ErrNoRows {
+ 290 | return Answer{}, false
+ 291 | } else if err != nil {
+ 292 | return Answer{}, false
+ 293 | }
+ 294 |
+ 295 | return answer, true
+ 296 | }
+ 297 |
+ 298 | func createAnswerWithText(conn *sql.DB, questionid int, text string, user AskUser) (Answer, error) {
+ 299 | query := "INSERT INTO answers (questionid, text, memberid, date_added) VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, questionid, text, gemlog_url, memberid, date_added"
+ 300 | row := conn.QueryRowContext(context.Background(), query, questionid, text, user.Id)
+ 301 | return scanAnswer(row)
+ 302 | }
+ 303 | func createAnswerAsGemlog(conn *sql.DB, questionid int, url string, user AskUser) (Answer, error) {
+ 304 | query := "INSERT INTO answers (questionid, gemlog_url, memberid, date_added) VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, questionid, text, gemlog_url, memberid, date_added"
+ 305 | row := conn.QueryRowContext(context.Background(), query, questionid, url, user.Id)
+ 306 | return scanAnswer(row)
+ 307 | }
+ 308 |
+ 309 | func updateAnswerText(conn *sql.DB, answer Answer, text string, user AskUser) (Answer, error) {
+ 310 | query := "UPDATE answers SET text=? WHERE id=? AND memberid=? RETURNING id, questionid, text, gemlog_url, memberid, date_added"
+ 311 | row := conn.QueryRowContext(context.Background(), query, text, answer.Id, user.Id)
+ 312 | return scanAnswer(row)
+ 313 | }
+ 314 |
+ 315 | func getUpvotesWithUsers(conn *sql.DB, answer Answer) []Upvote {
+ 316 | query := "SELECT upvotes.id, upvotes.answerid, upvotes.memberid, upvotes.date_added, members.id, members.username, members.language, members.timezone, members.is_staff, members.is_active, members.date_joined FROM upvotes LEFT JOIN members ON members.id=upvotes.memberid WHERE upvotes.answerid=? ORDER BY upvotes.date_added"
+ 317 | rows, rows_err := conn.QueryContext(context.Background(), query, answer.Id)
+ 318 |
+ 319 | var upvotes []Upvote
+ 320 | if rows_err == nil {
+ 321 | defer rows.Close()
+ 322 | for rows.Next() {
+ 323 | upvote, scan_err := scanUpvoteRowsWithUser(rows)
+ 324 | if scan_err == nil {
+ 325 | upvotes = append(upvotes, upvote)
+ 326 | } else {
+ 327 | panic(scan_err)
+ 328 | }
+ 329 | }
+ 330 | }
+ 331 |
+ 332 | return upvotes
+ 333 | }
+ 334 |
+ 335 | func addUpvote(conn *sql.DB, answer Answer, user AskUser) (Upvote, error) {
+ 336 | query := `UPDATE OR INSERT INTO upvotes (answerid, memberid, date_added)
+ 337 | VALUES (?, ?, CURRENT_TIMESTAMP)
+ 338 | MATCHING (answerid, memberid)
+ 339 | RETURNING id, answerid, memberid, date_added`
+ 340 | row := conn.QueryRowContext(context.Background(), query, answer.Id, user.Id)
+ 341 | return scanUpvote(row)
+ 342 | }
+ 343 |
+ 344 | func removeUpvote(conn *sql.DB, answer Answer, user AskUser) error {
+ 345 | query := `DELETE FROM upvotes WHERE answerid=? AND memberid=?`
+ 346 | _, err := conn.ExecContext(context.Background(), query, answer.Id, user.Id)
+ 347 | return err
+ 348 | }
gemini/ask/types.go (created)
+ 1 | package ask
+ 2 |
+ 3 | import (
+ 4 | "time"
+ 5 | "net/url"
+ 6 | "database/sql"
+ 7 | "strings"
+ 8 | )
+ 9 |
+ 10 | type AskUser struct {
+ 11 | Id int
+ 12 | Username string
+ 13 | Certificate AskUserCert
+ 14 | Language string
+ 15 | Timezone string
+ 16 | Is_staff bool
+ 17 | Is_active bool
+ 18 | Date_joined time.Time
+ 19 | }
+ 20 |
+ 21 | type AskUserCert struct {
+ 22 | Id int
+ 23 | MemberId int
+ 24 | Title string
+ 25 | Certificate string
+ 26 | Is_active bool
+ 27 | Date_added time.Time
+ 28 | }
+ 29 |
+ 30 | func ScanAskUserCert(row *sql.Row) AskUserCert {
+ 31 | cert := AskUserCert{}
+ 32 | var title interface{}
+ 33 | row.Scan(&cert.Id, &cert.MemberId, &title, &cert.Certificate, &cert.Is_active, &cert.Date_added)
+ 34 | if title != nil {
+ 35 | cert.Title = title.(string)
+ 36 | }
+ 37 |
+ 38 | return cert
+ 39 | }
+ 40 |
+ 41 | type Topic struct {
+ 42 | Id int
+ 43 | Title string
+ 44 | Description string
+ 45 | Date_added time.Time
+ 46 |
+ 47 | QuestionTotal int
+ 48 | }
+ 49 |
+ 50 | type Question struct {
+ 51 | Id int
+ 52 | TopicId int
+ 53 | Title string // Nullable
+ 54 | Text string // Nullable
+ 55 | Tags string // Nullable
+ 56 | MemberId int // Nullable
+ 57 | Date_added time.Time
+ 58 |
+ 59 | User AskUser
+ 60 | SelectedAnswer int
+ 61 | }
+ 62 |
+ 63 | func scanQuestion(row *sql.Row) (Question, error) {
+ 64 | question := Question{}
+ 65 | var title interface{}
+ 66 | var text interface{}
+ 67 | var tags interface{}
+ 68 | var memberid interface{}
+ 69 | err := row.Scan(&question.Id, &question.TopicId, &title, &text, &tags, &memberid, &question.Date_added)
+ 70 | if err != nil {
+ 71 | return Question{}, err
+ 72 | }
+ 73 | if title != nil {
+ 74 | if s, ok := title.([]uint8); ok {
+ 75 | question.Title = string(s)
+ 76 | } else if s, ok := title.(string); ok {
+ 77 | question.Title = s
+ 78 | }
+ 79 | }
+ 80 | if text != nil {
+ 81 | if s, ok := text.([]uint8); ok {
+ 82 | question.Text = string(s)
+ 83 | } else if s, ok := text.(string); ok {
+ 84 | question.Text = s
+ 85 | }
+ 86 | }
+ 87 | if tags != nil {
+ 88 | if s, ok := tags.([]uint8); ok {
+ 89 | question.Tags = string(s)
+ 90 | } else if s, ok := tags.(string); ok {
+ 91 | question.Tags = s
+ 92 | }
+ 93 | }
+ 94 | if memberid != nil {
+ 95 | question.MemberId = int(memberid.(int64))
+ 96 | }
+ 97 |
+ 98 | return question, nil
+ 99 | }
+ 100 |
+ 101 | func scanQuestionWithUser(row *sql.Row) (Question, error) {
+ 102 | question := Question{}
+ 103 | var title interface{}
+ 104 | var text interface{}
+ 105 | var tags interface{}
+ 106 | var memberid interface{}
+ 107 | err := row.Scan(&question.Id, &question.TopicId, &title, &text, &tags, &memberid, &question.Date_added, &question.User.Id, &question.User.Username, &question.User.Language, &question.User.Timezone, &question.User.Is_staff, &question.User.Is_active, &question.User.Date_joined)
+ 108 | if err != nil {
+ 109 | return Question{}, err
+ 110 | }
+ 111 | if title != nil {
+ 112 | if s, ok := title.([]uint8); ok {
+ 113 | question.Title = string(s)
+ 114 | } else if s, ok := title.(string); ok {
+ 115 | question.Title = s
+ 116 | }
+ 117 | }
+ 118 | if text != nil {
+ 119 | if s, ok := text.([]uint8); ok {
+ 120 | question.Text = string(s)
+ 121 | } else if s, ok := text.(string); ok {
+ 122 | question.Text = s
+ 123 | }
+ 124 | }
+ 125 | if tags != nil {
+ 126 | if s, ok := tags.([]uint8); ok {
+ 127 | question.Tags = string(s)
+ 128 | } else if s, ok := tags.(string); ok {
+ 129 | question.Tags = s
+ 130 | }
+ 131 | }
+ 132 | if memberid != nil {
+ 133 | question.MemberId = int(memberid.(int64))
+ 134 | }
+ 135 |
+ 136 | return question, nil
+ 137 | }
+ 138 |
+ 139 |
+ 140 | func scanQuestionRows(rows *sql.Rows) (Question, error) {
+ 141 | question := Question{}
+ 142 | var title interface{}
+ 143 | var text interface{}
+ 144 | var tags interface{}
+ 145 | var memberid interface{}
+ 146 | err := rows.Scan(&question.Id, &question.TopicId, &title, &text, &tags, &memberid, &question.Date_added)
+ 147 | if err != nil {
+ 148 | return Question{}, err
+ 149 | }
+ 150 | if title != nil {
+ 151 | if s, ok := title.([]uint8); ok {
+ 152 | question.Title = string(s)
+ 153 | } else if s, ok := title.(string); ok {
+ 154 | question.Title = s
+ 155 | }
+ 156 | }
+ 157 | if text != nil {
+ 158 | if s, ok := text.([]uint8); ok {
+ 159 | question.Text = string(s)
+ 160 | } else if s, ok := text.(string); ok {
+ 161 | question.Text = s
+ 162 | }
+ 163 | }
+ 164 | if tags != nil {
+ 165 | if s, ok := tags.([]uint8); ok {
+ 166 | question.Tags = string(s)
+ 167 | } else if s, ok := tags.(string); ok {
+ 168 | question.Tags = s
+ 169 | }
+ 170 | }
+ 171 | if memberid != nil {
+ 172 | question.MemberId = int(memberid.(int64))
+ 173 | }
+ 174 |
+ 175 | return question, nil
+ 176 | }
+ 177 | func scanQuestionRowsWithUser(rows *sql.Rows) (Question, error) {
+ 178 | question := Question{}
+ 179 | var title interface{}
+ 180 | var text interface{}
+ 181 | var tags interface{}
+ 182 | var memberid interface{}
+ 183 | err := rows.Scan(&question.Id, &question.TopicId, &title, &text, &tags, &memberid, &question.Date_added, &question.User.Id, &question.User.Username, &question.User.Language, &question.User.Timezone, &question.User.Is_staff, &question.User.Is_active, &question.User.Date_joined)
+ 184 | if err != nil {
+ 185 | return Question{}, err
+ 186 | }
+ 187 | if title != nil {
+ 188 | if s, ok := title.([]uint8); ok {
+ 189 | question.Title = string(s)
+ 190 | } else if s, ok := title.(string); ok {
+ 191 | question.Title = s
+ 192 | }
+ 193 | }
+ 194 | if text != nil {
+ 195 | if s, ok := text.([]uint8); ok {
+ 196 | question.Text = string(s)
+ 197 | } else if s, ok := text.(string); ok {
+ 198 | question.Text = s
+ 199 | }
+ 200 | }
+ 201 | if tags != nil {
+ 202 | if s, ok := tags.([]uint8); ok {
+ 203 | question.Tags = string(s)
+ 204 | } else if s, ok := tags.(string); ok {
+ 205 | question.Tags = s
+ 206 | }
+ 207 | }
+ 208 | if memberid != nil {
+ 209 | question.MemberId = int(memberid.(int64))
+ 210 | }
+ 211 |
+ 212 | return question, nil
+ 213 | }
+ 214 |
+ 215 | type Answer struct {
+ 216 | Id int
+ 217 | QuestionId int // Nullable
+ 218 | Text string // Nullable
+ 219 | Gemlog_url *url.URL // Nullable
+ 220 | MemberId int // Nullable
+ 221 | Date_added time.Time
+ 222 |
+ 223 | User AskUser
+ 224 | Upvotes int
+ 225 | }
+ 226 |
+ 227 | func scanAnswer(row *sql.Row) (Answer, error) {
+ 228 | answer := Answer{}
+ 229 | var text interface{}
+ 230 | var gemlog_url interface{}
+ 231 | var memberid interface{}
+ 232 | err := row.Scan(&answer.Id, &answer.QuestionId, &text, &gemlog_url, &memberid, &answer.Date_added)
+ 233 | if err != nil {
+ 234 | return (Answer{}), err
+ 235 | }
+ 236 | if text != nil {
+ 237 | if s, ok := text.([]uint8); ok {
+ 238 | answer.Text = string(s)
+ 239 | } else if s, ok := text.(string); ok {
+ 240 | answer.Text = s
+ 241 | }
+ 242 | }
+ 243 | if gemlog_url != nil {
+ 244 | var gemlog_url_string = string(gemlog_url.([]uint8))
+ 245 | var err2 error
+ 246 | answer.Gemlog_url, err2 = url.Parse(gemlog_url_string)
+ 247 | if err2 != nil {
+ 248 | // TODO
+ 249 | }
+ 250 | }
+ 251 | if memberid != nil {
+ 252 | answer.MemberId = int(memberid.(int64))
+ 253 | }
+ 254 |
+ 255 | return answer, nil
+ 256 | }
+ 257 |
+ 258 | func scanAnswerWithUser(row *sql.Row) (Answer, error) {
+ 259 | answer := Answer{}
+ 260 | var text interface{}
+ 261 | var gemlog_url interface{}
+ 262 | var memberid interface{}
+ 263 | err := row.Scan(&answer.Id, &answer.QuestionId, &text, &gemlog_url, &memberid, &answer.Date_added, &answer.User.Id, &answer.User.Username, &answer.User.Language, &answer.User.Timezone, &answer.User.Is_staff, &answer.User.Is_active, &answer.User.Date_joined)
+ 264 | if err != nil {
+ 265 | return (Answer{}), err
+ 266 | }
+ 267 | if text != nil {
+ 268 | if s, ok := text.([]uint8); ok {
+ 269 | answer.Text = string(s)
+ 270 | } else if s, ok := text.(string); ok {
+ 271 | answer.Text = s
+ 272 | }
+ 273 | }
+ 274 | if gemlog_url != nil {
+ 275 | var gemlog_url_string = string(gemlog_url.([]uint8))
+ 276 | var err2 error
+ 277 | answer.Gemlog_url, err2 = url.Parse(gemlog_url_string)
+ 278 | if err2 != nil {
+ 279 | // TODO
+ 280 | }
+ 281 | }
+ 282 | if memberid != nil {
+ 283 | answer.MemberId = int(memberid.(int64))
+ 284 | }
+ 285 |
+ 286 | return answer, nil
+ 287 | }
+ 288 |
+ 289 | // With User
+ 290 | func scanAnswerRows(rows *sql.Rows) (Answer, error) {
+ 291 | answer := Answer{}
+ 292 | var text interface{}
+ 293 | var gemlog_url interface{}
+ 294 | var memberid interface{}
+ 295 | err := rows.Scan(&answer.Id, &answer.QuestionId, &text, &gemlog_url, &memberid, &answer.Date_added, &answer.User.Id, &answer.User.Username, &answer.User.Language, &answer.User.Timezone, &answer.User.Is_staff, &answer.User.Is_active, &answer.User.Date_joined)
+ 296 | if err != nil {
+ 297 | return (Answer{}), err
+ 298 | }
+ 299 | if text != nil {
+ 300 | if s, ok := text.([]uint8); ok {
+ 301 | answer.Text = string(s)
+ 302 | } else if s, ok := text.(string); ok {
+ 303 | answer.Text = s
+ 304 | }
+ 305 | }
+ 306 | if gemlog_url != nil {
+ 307 | var gemlog_url_string = string(gemlog_url.([]uint8))
+ 308 | var err2 error
+ 309 | answer.Gemlog_url, err2 = url.Parse(gemlog_url_string)
+ 310 | if err2 != nil {
+ 311 | // TODO
+ 312 | }
+ 313 | }
+ 314 | if memberid != nil {
+ 315 | answer.MemberId = int(memberid.(int64))
+ 316 | }
+ 317 |
+ 318 | return answer, nil
+ 319 | }
+ 320 |
+ 321 | type Activity struct {
+ 322 | Q Question
+ 323 | Activity string
+ 324 | Activity_date time.Time
+ 325 | AnswerId int
+ 326 | User AskUser
+ 327 | TopicTitle string
+ 328 | }
+ 329 |
+ 330 |
+ 331 | func scanActivityWithUser(rows *sql.Rows) (Activity, error) {
+ 332 | activity := Activity{}
+ 333 | var title interface{}
+ 334 | var text interface{}
+ 335 | var tags interface{}
+ 336 | var memberid interface{}
+ 337 | err := rows.Scan(&activity.Q.Id, &activity.Q.TopicId, &title, &text, &tags, &memberid, &activity.Activity, &activity.Activity_date, &activity.AnswerId, &activity.User.Id, &activity.User.Username, &activity.User.Language, &activity.User.Timezone, &activity.User.Is_staff, &activity.User.Is_active, &activity.User.Date_joined, &activity.TopicTitle)
+ 338 | if err != nil {
+ 339 | return Activity{}, err
+ 340 | }
+ 341 | if title != nil {
+ 342 | activity.Q.Title = string(title.([]uint8))
+ 343 | }
+ 344 | if text != nil {
+ 345 | activity.Q.Text = text.(string)
+ 346 | }
+ 347 | if tags != nil {
+ 348 | activity.Q.Tags = string(tags.([]uint8))
+ 349 | }
+ 350 | if memberid != nil {
+ 351 | activity.Q.MemberId = int(memberid.(int64))
+ 352 | }
+ 353 |
+ 354 | activity.Activity = strings.TrimSpace(activity.Activity)
+ 355 |
+ 356 | return activity, nil
+ 357 | }
+ 358 |
+ 359 | type QuestionComment struct {
+ 360 | Id int
+ 361 | QuestionId int
+ 362 | Text string
+ 363 | MemberId int
+ 364 | Date_added time.Time
+ 365 | }
+ 366 |
+ 367 | type AnswerComment struct {
+ 368 | Id int
+ 369 | AnswerId int
+ 370 | Text string
+ 371 | MemberId int
+ 372 | Date_added time.Time
+ 373 | }
+ 374 |
+ 375 | type Upvote struct {
+ 376 | Id int
+ 377 | AnswerId int
+ 378 | MemberId int
+ 379 | Date_added time.Time
+ 380 |
+ 381 | User AskUser
+ 382 | }
+ 383 |
+ 384 | func scanUpvote(row *sql.Row) (Upvote, error) {
+ 385 | upvote := Upvote{}
+ 386 | var memberid interface{}
+ 387 | err := row.Scan(&upvote.Id, &upvote.AnswerId, &memberid, &upvote.Date_added)
+ 388 | if err != nil {
+ 389 | return (Upvote{}), err
+ 390 | }
+ 391 | if memberid != nil {
+ 392 | upvote.MemberId = int(memberid.(int64))
+ 393 | }
+ 394 |
+ 395 | return upvote, nil
+ 396 | }
+ 397 |
+ 398 | func scanUpvoteRowsWithUser(rows *sql.Rows) (Upvote, error) {
+ 399 | upvote := Upvote{}
+ 400 | var memberid interface{}
+ 401 | err := rows.Scan(&upvote.Id, &upvote.AnswerId, &memberid, &upvote.Date_added, &upvote.User.Id, &upvote.User.Username, &upvote.User.Language, &upvote.User.Timezone, &upvote.User.Is_staff, &upvote.User.Is_active, &upvote.User.Date_joined)
+ 402 | if err != nil {
+ 403 | return (Upvote{}), err
+ 404 | }
+ 405 | if memberid != nil {
+ 406 | upvote.MemberId = int(memberid.(int64))
+ 407 | }
+ 408 |
+ 409 | return upvote, nil
+ 410 | }
gemini/chat/chat.go
... | ...
33 | username string
34 | clientNumber atomic.Int64
35 | }
36 |
37 | func HandleChat(s sis.ServerHandle) {
+ 38 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
+ 39 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
38 | context := ChatContext{
39 | changeInt: 0,
40 | mutex: sync.RWMutex{},
41 | //messages: deque.New[ChatText](0, 30),
42 | messages: make([]ChatText, 0, 100),
... | ...
63 | })
64 | s.AddRoute("/chat/:username", func(request sis.Request) {
65 | username := request.GetParam("username")
66 | if username == "" {
67 | request.Redirect("/chat/")
+ 70 | return
+ 71 | }
+ 72 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# AuraGem Live Chat\nThis chat is heavily inspired by Mozz's chat, but the UI has been tailored for most gemini browsers. Message history is cleared every 24 hours. This chat makes use of keepalive packets so that clients (that support them) will not timeout.\n"})
+ 73 | if request.ScrollMetadataRequested {
+ 74 | request.SendAbstract("")
68 | return
69 | }
70 | var builder strings.Builder
71 | fmt.Fprintf(&builder, "# Live Chat\nThis chat is heavily inspired by Mozz's chat, but the UI has been tailored for most gemini browsers. Message history is cleared every 24 hours. This chat makes use of keepalive packets so that clients (that support them) will not timeout.\n=> gemini://chat.mozz.us/ Mozz's Chat\n\n")
72 | fmt.Fprintf(&builder, "=> /chat/%s/send Send Message\n=> titan://auragem.letz.dev/chat/%s/send Send Message via Titan\n\n", url.PathEscape(username), url.PathEscape(username))
... | ...
176 | message = strings.ReplaceAll(message, "\n###", "")
177 | message = strings.ReplaceAll(message, "\n##", "")
178 | message = strings.ReplaceAll(message, "\n#", "")
179 | message = strings.ReplaceAll(message, "\n-[", "")
180 |
- 181 | sendChan <- (ChatText{username, message, time.Now(), ""})
+ 188 | if !request.ScrollMetadataRequested {
+ 189 | sendChan <- (ChatText{username, message, time.Now(), ""})
+ 190 | }
182 | //return c.NoContent(gig.StatusRedirectTemporary, "gemini://auragem.letz.dev/chat/"+url.PathEscape(username))
... | ...
178 | //return c.NoContent(gig.StatusRedirectTemporary, "gemini://auragem.letz.dev/chat/"+url.PathEscape(username))
- 183 | request.Redirect("gemini://auragem.letz.dev/chat/" + url.PathEscape(username))
+ 192 | request.Redirect(request.Server.Scheme() + "auragem.letz.dev/chat/" + url.PathEscape(username))
184 | }
185 |
186 | s.AddRoute("/chat/:username/send", sendFunc)
187 | s.AddUploadRoute("/chat/:username/send", sendFunc) // Titan Upload
188 |
gemini/devlog.go
... | ...
-1 | package gemini
0 |
1 | import (
+ 4 | "time"
+ 5 |
4 | utils "gitlab.com/clseibold/auragem_sis/gemini/utils"
5 | sis "gitlab.com/clseibold/smallnetinformationservices"
6 | )
7 |
8 | func handleDevlog(s sis.ServerHandle) {
... | ...
4 | utils "gitlab.com/clseibold/auragem_sis/gemini/utils"
5 | sis "gitlab.com/clseibold/smallnetinformationservices"
6 | )
7 |
8 | func handleDevlog(s sis.ServerHandle) {
- 9 | /*g.AddRoute("/~krixano/gemlog/atom.xml", func(request sis.Request) {
- 10 | // c.NoContent(gig.StatusRedirectTemporary, "/devlog/atom.xml")
- 11 | request.TextWithMimetype("text/xml", generateAtomFrom("SIS/gemini/devlog/index.gmi", "gemini://auragem.letz.dev", "gemini://auragem.letz.dev/devlog", "Christian \"Krixano\" Seibold", "christian.seibold32@outlook.com"))
- 12 | })*/
- 13 |
+ 11 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2021-04-24T00:00:00", time.Local)
14 | s.AddRoute("/devlog/atom.xml", func(request sis.Request) {
... | ...
10 | s.AddRoute("/devlog/atom.xml", func(request sis.Request) {
- 15 | request.TextWithMimetype("text/xml", utils.GenerateAtomFrom("SIS/auragem_gemini/devlog/index.gmi", "gemini://auragem.letz.dev", "gemini://auragem.letz.dev/devlog", "Christian \"Krixano\" Seibold", "krixano@protonmail.com"))
+ 13 | atom, feedTitle, lastUpdate := utils.GenerateAtomFrom("SIS/auragem_gemini/devlog/index.gmi", "gemini://auragem.letz.dev", "gemini://auragem.letz.dev/devlog", "Christian Lee Seibold", "christian.seibold32@outlook.com")
+ 14 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# " + feedTitle + "\n"})
+ 15 | if request.ScrollMetadataRequested {
+ 16 | request.SendAbstract("text/xml")
+ 17 | return
+ 18 | }
+ 19 | request.TextWithMimetype("text/xml", atom)
16 | })
17 | }
gemini/gemini.go
... | ...
9 | "unicode"
10 | "unicode/utf8"
11 |
12 | "github.com/spf13/cobra"
13 |
+ 14 | "gitlab.com/clseibold/auragem_sis/gemini/ask"
14 | "gitlab.com/clseibold/auragem_sis/gemini/chat"
15 | "gitlab.com/clseibold/auragem_sis/gemini/music"
16 | "gitlab.com/clseibold/auragem_sis/gemini/search"
... | ...
12 | "gitlab.com/clseibold/auragem_sis/gemini/chat"
13 | "gitlab.com/clseibold/auragem_sis/gemini/music"
14 | "gitlab.com/clseibold/auragem_sis/gemini/search"
+ 18 | "gitlab.com/clseibold/auragem_sis/gemini/starwars"
17 | "gitlab.com/clseibold/auragem_sis/gemini/textgame"
18 | "gitlab.com/clseibold/auragem_sis/gemini/textola"
19 | "gitlab.com/clseibold/auragem_sis/gemini/texts"
... | ...
15 | "gitlab.com/clseibold/auragem_sis/gemini/textgame"
16 | "gitlab.com/clseibold/auragem_sis/gemini/textola"
17 | "gitlab.com/clseibold/auragem_sis/gemini/texts"
- 20 | youtube "gitlab.com/clseibold/auragem_sis/gemini/youtube"
+ 22 | "gitlab.com/clseibold/auragem_sis/gemini/youtube"
21 | sis "gitlab.com/clseibold/smallnetinformationservices"
22 | // "gitlab.com/clseibold/auragem_sis/lifekept"
23 | /*"gitlab.com/clseibold/auragem_sis/ask"
24 | "gitlab.com/clseibold/auragem_sis/music"
25 | "gitlab.com/clseibold/auragem_sis/search"
... | ...
30 | Short: "Start SIS",
31 | Run: RunServer,
32 | }
33 |
34 | func RunServer(cmd *cobra.Command, args []string) {
- 35 | //f, _ := os.Create("access.log")
- 36 | //gig.DefaultWriter = io.MultiWriter(f, os.Stdout)
- 37 |
38 | context, err := sis.InitSIS("./SIS/")
... | ...
34 | context, err := sis.InitSIS("./SIS/")
+ 38 | if err != nil {
+ 39 | panic(err)
+ 40 | }
39 | context.AdminServer().BindAddress = "0.0.0.0"
40 | context.AdminServer().Hostname = "auragem.letz.dev"
41 | context.AdminServer().AddCertificate("auragem.pem")
42 | context.SaveConfiguration()
43 | context.GetPortListener("0.0.0.0", "1995").AddCertificate("auragem.letz.dev", "auragem.pem")
... | ...
39 | context.AdminServer().BindAddress = "0.0.0.0"
40 | context.AdminServer().Hostname = "auragem.letz.dev"
41 | context.AdminServer().AddCertificate("auragem.pem")
42 | context.SaveConfiguration()
43 | context.GetPortListener("0.0.0.0", "1995").AddCertificate("auragem.letz.dev", "auragem.pem")
- 44 | if err != nil {
- 45 | panic(err)
- 46 | }
47 |
... | ...
43 |
- 48 | // ----- AuraGem Servers -----
+ 47 | setupAuraGem(context)
+ 48 | setupScholasticDiversity(context)
+ 49 | setupScrollProtocol(context)
+ 50 |
+ 51 | context.Start()
+ 52 | }
49 |
... | ...
45 |
- 50 | geminiServer := context.AddServer(sis.Server{Type: sis.ServerType_Gemini, Name: "auragem_gemini", Hostname: "auragem.letz.dev"})
+ 54 | func setupAuraGem(context *sis.SISContext) {
+ 55 | geminiServer := context.AddServer(sis.Server{Type: sis.ServerType_Gemini, Name: "auragem_gemini", Hostname: "auragem.letz.dev", DefaultLanguage: "en"})
51 | context.GetPortListener("0.0.0.0", "1965").AddCertificate("auragem.letz.dev", "auragem.pem")
52 |
53 | geminiServer.AddDirectory("/*", "./")
54 | geminiServer.AddFile("/.well-known/security.txt", "./security.txt")
55 | geminiServer.AddProxyRoute("/nex/*", "$auragem_nex/*", '1')
... | ...
51 | context.GetPortListener("0.0.0.0", "1965").AddCertificate("auragem.letz.dev", "auragem.pem")
52 |
53 | geminiServer.AddDirectory("/*", "./")
54 | geminiServer.AddFile("/.well-known/security.txt", "./security.txt")
55 | geminiServer.AddProxyRoute("/nex/*", "$auragem_nex/*", '1')
- 56 |
- 57 | // Guestbook via Titan
58 | geminiServer.AddUploadRoute("/guestbook.gmi", handleGuestbook)
59 |
... | ...
55 | geminiServer.AddUploadRoute("/guestbook.gmi", handleGuestbook)
56 |
+ 63 | // Proxies
+ 64 | youtube.HandleYoutube(geminiServer)
+ 65 | handleGithub(geminiServer)
+ 66 | // twitch.HandleTwitch(geminiServer)
+ 67 |
+ 68 | // Services
60 | handleDevlog(geminiServer)
... | ...
56 | handleDevlog(geminiServer)
- 61 | youtube.HandleYoutube(geminiServer)
62 | handleWeather(geminiServer)
... | ...
58 | handleWeather(geminiServer)
- 63 | handleGithub(geminiServer)
64 | textgame.HandleTextGame(geminiServer)
65 | chat.HandleChat(geminiServer)
66 | textola.HandleTextola(geminiServer)
67 | music.HandleMusic(geminiServer)
68 | search.HandleSearchEngine(geminiServer)
... | ...
64 | textgame.HandleTextGame(geminiServer)
65 | chat.HandleChat(geminiServer)
66 | textola.HandleTextola(geminiServer)
67 | music.HandleMusic(geminiServer)
68 | search.HandleSearchEngine(geminiServer)
- 69 | // twitch.HandleTwitch(geminiServer)
- 70 | // ask.HandleAsk(geminiServer)
- 71 |
- 72 | nexServer := context.AddServer(sis.Server{Type: sis.ServerType_Nex, Name: "auragem_nex", Hostname: "auragem.letz.dev"})
- 73 | nexServer.AddDirectory("/*", "./")
- 74 | nexServer.AddProxyRoute("/gemini/*", "$auragem_gemini/*", '1')
- 75 | nexServer.AddProxyRoute("/scholasticdiversity/*", "$scholasticdiversity_gemini/*", '1')
+ 76 | starwars.HandleStarWars(geminiServer)
+ 77 | ask.HandleAsk(geminiServer)
76 |
... | ...
72 |
- 77 | // ----- Scholastic Diversity stuff -----
- 78 | scholasticdiversity_gemini := context.AddServer(sis.Server{Type: sis.ServerType_Gemini, Name: "scholasticdiversity_gemini", Hostname: "scholasticdiversity.us.to"})
- 79 | context.GetPortListener("0.0.0.0", "1965").AddCertificate("scholasticdiversity.us.to", "scholasticdiversity.pem")
- 80 | scholasticdiversity_gemini.AddDirectory("/*", "./")
- 81 |
- 82 | texts.HandleTexts(scholasticdiversity_gemini)
83 | // Add "/texts/" redirect from auragem gemini server to scholastic diversity gemini server
84 | geminiServer.AddRoute("/texts/*", func(request sis.Request) {
85 | unescaped, err := url.PathUnescape(request.GlobString)
86 | if err != nil {
87 | request.TemporaryFailure(err.Error())
... | ...
88 | return
89 | }
90 | request.Redirect("gemini://scholasticdiversity.us.to/scriptures/%s", unescaped)
91 | })
92 |
- 93 | gopherServer := context.AddServer(sis.Server{Type: sis.ServerType_Gopher, Name: "gopher", Hostname: "auragem.letz.dev"})
- 94 | gopherServer.AddRoute("/", func(request sis.Request) {
- 95 | request.GophermapLine("i", " AuraGem Gopher Server", "/", "", "")
- 96 | request.GophermapLine("i", "", "/", "", "")
- 97 | request.GophermapLine("0", "About this server", "/about.txt", "", "")
- 98 | request.GophermapLine("i", "", "/", "", "")
- 99 | request.GophermapLine("7", "Search Geminispace", "/g/search/s/", "", "")
- 100 | request.GophermapLine("1", "Devlog", "/g/devlog/", "", "")
- 101 | request.GophermapLine("1", "Personal Log", "/g/~clseibold/", "", "")
- 102 | request.GophermapLine("0", "My Experience Within the Bitreich IRC", "/on_bitreich.txt", "", "")
- 103 | request.GophermapLine("0", "Freedom of Protocols Initiative", "/freedom_of_protocols_initiative.txt", "", "")
- 104 | request.GophermapLine("i", "", "/", "", "")
- 105 |
- 106 | request.GophermapLine("i", "Services/Info", "/", "", "")
- 107 | request.GophermapLine("1", "AuraGem Public Radio", "/g/music/public_radio/", "", "")
- 108 | request.GophermapLine("1", "Search Engine Homepage", "/g/search/", "", "")
- 109 | request.GophermapLine("1", "YouTube Proxy", "/g/youtube/", "", "")
- 110 | request.GophermapLine("1", "Scholastic Diversity", "/scholasticdiversity/", "", "")
- 111 | request.GophermapLine("i", "", "/", "", "")
- 112 |
- 113 | request.GophermapLine("i", "Software", "/", "", "")
- 114 | request.GophermapLine("1", "Misfin-Server", "/g/misfin-server/", "", "")
- 115 | request.GophermapLine("i", "", "/", "", "")
- 116 |
- 117 | request.GophermapLine("i", "Links", "/", "", "")
- 118 | request.GophermapLine("1", "Gopher Starting Point", "/iOS/gopher", "forthworks.com", "70")
- 119 | request.GophermapLine("7", "Search Via Veronica-2", "/v2/vs", "gopher.floodgap.com", "70")
- 120 | request.GophermapLine("7", "Search Via Quarry", "/quarry", "gopher.icu", "70")
- 121 | request.GophermapLine("1", "Gopherpedia", "/", "gopherpedia.com", "70")
- 122 | request.GophermapLine("1", "Bongusta Phlog Aggregator", "/bongusta", "i-logout.cz", "70")
- 123 | request.GophermapLine("1", "Moku Pona Phlog Aggregator", "/moku-pona", "gopher.black", "70")
- 124 | request.GophermapLine("1", "Mare Tranquillitatis People's Circumlunar Zaibatsu", "/", "zaibatsu.circumlunar.space", "70")
- 125 | request.GophermapLine("1", "Cosmic Voyage", "/", "cosmic.voyage", "70")
- 126 | request.GophermapLine("1", "Mozz.Us", "/", "mozz.us", "70")
- 127 | request.GophermapLine("1", "Quux", "/", "gopher.quux.org", "70")
- 128 | request.GophermapLine("1", "Mateusz' gophre lair", "/", "gopher.viste.fr", "70")
- 129 | request.GophermapLine("i", "", "/", "", "")
+ 89 | scrollServer := context.AddServer(sis.Server{Type: sis.ServerType_Scroll, Name: "auragem_scroll", Hostname: "auragem.letz.dev", DefaultLanguage: "en"})
+ 90 | context.GetPortListener("0.0.0.0", "5699").AddCertificate("auragem.letz.dev", "auragem.pem")
+ 91 | scrollServer.AddProxyRoute("/*", "$auragem_gemini/*", '1')
130 |
... | ...
126 |
- 131 | request.GophermapLine("i", "Sister Sites", "/", "", "")
- 132 | request.GophermapLine("h", "AuraGem Gemini Server", "URL:gemini://auragem.letz.dev", "", "")
- 133 | request.GophermapLine("h", "AuraGem Nex Server", "URL:nex://auragem.letz.dev", "", "")
- 134 | request.GophermapLine("h", "Scholastic Diversity Gemini Server", "URL:gemini://scholasticdiversity.us.to", "", "")
- 135 | request.GophermapLine("i", "", "/", "", "")
+ 93 | nexServer := context.AddServer(sis.Server{Type: sis.ServerType_Nex, Name: "auragem_nex", Hostname: "auragem.letz.dev", DefaultLanguage: "en"})
+ 94 | nexServer.AddDirectory("/*", "./")
+ 95 | nexServer.AddProxyRoute("/gemini/*", "$auragem_gemini/*", '1')
+ 96 | nexServer.AddProxyRoute("/scholasticdiversity/*", "$scholasticdiversity_gemini/*", '1')
+ 97 | nexServer.AddProxyRoute("/scrollprotocol/*", "$scrollprotocol_gemini/*", '1')
136 |
... | ...
132 |
- 137 | request.GophermapLine("i", "Ways to Contact Me:", "", "", "")
- 138 | request.GophermapLine("i", "IRC: ##misfin on libera.chat", "/", "", "")
- 139 | request.GophermapLine("h", "Email", "URL:mailto:christian.seibold32@outlook.com", "", "")
- 140 | request.GophermapLine("h", "Misfin Mail", "URL:misfin://clseibold@auragem.letz.dev", "", "")
- 141 | request.GophermapLine("i", "", "/", "", "")
+ 99 | spartanServer := context.AddServer(sis.Server{Type: sis.ServerType_Spartan, Name: "spartan", Hostname: "auragem.letz.dev", DefaultLanguage: "en"})
+ 100 | spartanServer.AddFile("/", "./index.gmi")
+ 101 | spartanServer.AddProxyRoute("/*", "$auragem_gemini/*", '1')
142 |
... | ...
138 |
- 143 | request.GophermapLine("i", "Powered By", "/", "", "")
- 144 | request.GophermapLine("i", "This server is powered by Smallnet Information Services (SIS):", "/", "", "")
- 145 | request.GophermapLine("h", "SIS Project", "URL:https://gitlab.com/clseibold/smallnetinformationservices/", "", "")
- 146 | request.GophermapLine("i", "Note that while SIS docs use the term \"proxying\" to describe requests of one server being handed off to another server of a different protocol, this is not proxying proper. The default document format of the protocol (index gemtext files, gophermaps, and Nex Listings) is translated when needed, but that and links are the only conversions that happen. This form of \"proxying\" all happens internally in the server software and *not* over the network or sockets. It is functionally equivalent to protocol proxying, but works slightly differently.", "/", "", "")
- 147 | request.GophermapLine("i", "", "/", "", "")
- 148 | })
+ 103 | gopherServer := context.AddServer(sis.Server{Type: sis.ServerType_Gopher, Name: "gopher", Hostname: "auragem.letz.dev", DefaultLanguage: "en"})
149 | gopherServer.AddDirectory("/*", "./")
150 | gopherServer.AddProxyRoute("/g/*", "$auragem_gemini/*", '1')
151 | gopherServer.AddProxyRoute("/scholasticdiversity/*", "$scholasticdiversity_gemini/*", '1')
... | ...
147 | gopherServer.AddDirectory("/*", "./")
148 | gopherServer.AddProxyRoute("/g/*", "$auragem_gemini/*", '1')
149 | gopherServer.AddProxyRoute("/scholasticdiversity/*", "$scholasticdiversity_gemini/*", '1')
- 152 |
- 153 | spartanServer := context.AddServer(sis.Server{Type: sis.ServerType_Spartan, Name: "spartan", Hostname: "auragem.letz.dev"})
- 154 | spartanServer.AddRoute("/", func(request sis.Request) {
- 155 | request.Gemini(`# AuraGem Spartan Server
+ 107 | gopherServer.AddProxyRoute("/scrollprotocol/*", "$scrollprotocol_scroll/*", '1')
+ 108 | }
156 |
... | ...
152 |
- 157 | =: /g/search/s/ 🔍 Search
- 158 | => /g/devlog/ Devlog
- 159 | => /g/~clseibold/ Personal Log
- 160 | => /g/music/public_radio/ AuraGem Public Radio
+ 110 | func setupScholasticDiversity(context *sis.SISContext) {
+ 111 | scholasticdiversity_gemini := context.AddServer(sis.Server{Type: sis.ServerType_Gemini, Name: "scholasticdiversity_gemini", Hostname: "scholasticdiversity.us.to", DefaultLanguage: "en"})
+ 112 | context.GetPortListener("0.0.0.0", "1965").AddCertificate("scholasticdiversity.us.to", "scholasticdiversity.pem")
+ 113 | scholasticdiversity_gemini.AddDirectory("/*", "./")
161 |
... | ...
157 |
- 162 | ## Software
- 163 | => /g/misfin-server/ Misfin-Server
+ 115 | texts.HandleTexts(scholasticdiversity_gemini)
164 |
... | ...
160 |
- 165 | ## Other
- 166 | => /g/search/ Search Engine Homepage
- 167 | => /g/youtube/ YouTube Proxy
+ 117 | scholasticdiversity_scroll := context.AddServer(sis.Server{Type: sis.ServerType_Scroll, Name: "scholasticdiversity_scroll", Hostname: "scholasticdiversity.us.to", DefaultLanguage: "en"})
+ 118 | context.GetPortListener("0.0.0.0", "5699").AddCertificate("scholasticdiversity.us.to", "scholasticdiversity.pem")
+ 119 | scholasticdiversity_scroll.AddProxyRoute("/*", "$scholasticdiversity_gemini/*", '1')
+ 120 | }
168 |
... | ...
164 |
- 169 | ## Sister Sites
- 170 | => gemini://auragem.letz.dev/ AuraGem Gemini Server
- 171 | => nex://auragem.letz.dev/ AuraGem Nex Server
- 172 | => gemini://scholasticdiversity.us.to/ Scholastic Diversity
+ 122 | func setupScrollProtocol(context *sis.SISContext) {
+ 123 | scrollProtocol_scroll := context.AddServer(sis.Server{Type: sis.ServerType_Scroll, Name: "scrollprotocol_scroll", Hostname: "scrollprotocol.us.to", DefaultLanguage: "en"})
+ 124 | context.GetPortListener("0.0.0.0", "5699").AddCertificate("scrollprotocol.us.to", "scrollprotocol.pem")
+ 125 | scrollProtocol_scroll.AddDirectory("/*", "/")
173 |
... | ...
169 |
- 174 | ## Powered By
- 175 | This server is powered by Smallnet Information Services (SIS):
- 176 | => https://gitlab.com/clseibold/smallnetinformationservices/ SIS Project
- 177 | `)
- 178 | })
- 179 | spartanServer.AddProxyRoute("/g/*", "$auragem_gemini/*", '1')
- 180 |
- 181 | context.Start()
+ 127 | scrollProtocol_gemini := context.AddServer(sis.Server{Type: sis.ServerType_Gemini, Name: "scrollprotocol_gemini", Hostname: "scrollprotocol.us.to", DefaultLanguage: "en"})
+ 128 | context.GetPortListener("0.0.0.0", "1965").AddCertificate("scrollprotocol.us.to", "scrollprotocol.pem")
+ 129 | scrollProtocol_gemini.AddProxyRoute("/*", "$scrollprotocol_scroll/*", '1')
182 | }
183 |
184 | func handleGuestbook(request sis.Request) {
185 | guestbookPrefix := `# AuraGem Guestbook
186 |
gemini/music/music.go
... | ...
6 | "database/sql"
7 | "fmt"
8 | "net/url"
9 | "os"
10 | "path/filepath"
+ 11 | "strconv"
11 | "strings"
12 | "time"
... | ...
8 | "strings"
9 | "time"
+ 14 |
+ 15 | _ "embed"
13 |
14 | "github.com/dhowden/tag"
15 | "gitlab.com/clseibold/auragem_sis/config"
16 | "gitlab.com/clseibold/auragem_sis/db"
17 | sis "gitlab.com/clseibold/smallnetinformationservices"
... | ...
36 |
37 | => /music/register Register Page
38 | => /music/quota How the Quota System Works
39 | `
40 |
+ 44 | //go:embed music_index.gmi
+ 45 | var index_gmi string
+ 46 |
+ 47 | //go:embed music_index_scroll.scroll
+ 48 | var index_scroll string
+ 49 |
41 | func HandleMusic(s sis.ServerHandle) {
... | ...
37 | func HandleMusic(s sis.ServerHandle) {
+ 51 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2022-07-15T00:00:00", time.Local)
+ 52 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
42 | // ffmpeg (goav) Stuff: Register all formats and codecs
43 | // avformat.AvRegisterAll()
44 | // avcodec.AvcodecRegisterAll()
45 |
46 | // Database Connection
... | ...
56 | //defer throttlePool.ReleasePool()
57 |
58 | handleRadioService(s, conn)
59 |
60 | s.AddRoute("/music/", func(request sis.Request) {
+ 72 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# AuraGem Music\nA music service where you can upload a limited number of mp3s over Titan and listen to your private music library over Scroll/Gemini/Spartan. Stream individual songs or full albums, or use the \"Shuffled Stream\" feature that acts like a private radio of random songs from your library.\n"})
+ 73 | if request.ScrollMetadataRequested {
+ 74 | request.SendAbstract("")
+ 75 | return
+ 76 | }
+ 77 |
61 | cert := request.UserCert
62 | if cert == nil {
... | ...
58 | cert := request.UserCert
59 | if cert == nil {
- 63 | request.Gemini(`# AuraGem Music
- 64 |
- 65 | Welcome to the new AuraGem Music Service, where you can upload a limited number of mp3s over Titan and listen to your private music library over Gemini.
- 66 |
- 67 | Note: Remember to make sure your certificate is selected on this page if you've already registered.
- 68 |
- 69 | In order to register, create and enable a client certificate and then head over to the Register Cert page:
- 70 |
- 71 | => /music/register Register Cert
- 72 | => /music/quota How the Quota System Works
- 73 | => gemini://transjovian.org/titan About Titan
- 74 |
- 75 | => /music/public_radio/ Public Radio
- 76 |
- 77 | ## Features
- 78 |
- 79 | * Upload MP3s
- 80 | * Music organized by Artist and Album
- 81 | * Stream a full album or a particular artist's songs
- 82 | * "Shuffled stream" - infinite stream of random songs from user's private library
- 83 | * Delete songs from library
- 84 |
- 85 | ## The Legality of AuraGem Music
- 86 |
- 87 | AuraGem Music is currently not a distribution platform. Instead, it hosts a user's personal collection of music for their own consumption. Therefore, the music a user uploads is only visible to that person and will not be distributed to others. Uploading music that the user has bought is legitimate. However, the user is solely responsible for any pirated content that they have uploaded.
- 88 | `)
+ 80 | if request.Type == sis.ServerType_Gemini {
+ 81 | request.Gemini(index_gmi)
+ 82 | } else if request.Type == sis.ServerType_Scroll {
+ 83 | request.Scroll(index_scroll)
+ 84 | } else if request.Type == sis.ServerType_Spartan {
+ 85 | request.TemporaryFailure("Service not available over Spartan. Please visit over Gemini or Scroll.")
+ 86 | } else if request.Type == sis.ServerType_Nex {
+ 87 | request.TemporaryFailure("Service not available over Nex. Please visit over Gemini or Scroll.")
+ 88 | } else if request.Type == sis.ServerType_Gopher {
+ 89 | request.TemporaryFailure("Service not available over Gopher. Please visit over Gemini or Scroll.")
+ 90 | }
89 | return
90 | } else {
91 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
92 | if !isRegistered {
93 | request.Gemini(registerNotification)
... | ...
91 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
92 | if !isRegistered {
93 | request.Gemini(registerNotification)
94 | return
95 | } else {
+ 98 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# AuraGem Music - " + user.Username + "\n"})
+ 99 | if request.ScrollMetadataRequested {
+ 100 | request.SendAbstract("")
+ 101 | return
+ 102 | }
96 | getUserDashboard(request, conn, user)
97 | return
98 | }
99 | }
100 | })
... | ...
98 | }
99 | }
100 | })
101 |
102 | s.AddRoute("/music/quota", func(request sis.Request) {
+ 110 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# AuraGem Music - How the Quota System Works\nDescribes the quota system.\n"})
+ 111 | if request.ScrollMetadataRequested {
+ 112 | request.SendAbstract("")
+ 113 | return
+ 114 | }
103 | template := `# AuraGem Music - How the Quota System Works
104 |
105 | Each song adds to your quota 1 divided by the number of people who have uploaded that same song. If 3 people have uploaded the 3 same songs, only 1 song gets added to each person's quota (3 songs / 3 uploaders). However, if you are the only person who has uploaded a song, then 1 will be added to your quota (1 song / 1 uploader). The maximum quota that each user has is currently set to %d.
106 |
107 | Note that the below calculations assume an average mp3 file size of 7.78 MB per song:
... | ...
114 | `
115 | request.Gemini(fmt.Sprintf(template, userSongQuota))
116 | })
117 |
118 | s.AddRoute("/music/about", func(request sis.Request) {
+ 131 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# About AuraGem Music\n"})
+ 132 | if request.ScrollMetadataRequested {
+ 133 | request.SendAbstract("")
+ 134 | return
+ 135 | }
119 | template := `# About AuraGem Music
120 |
121 | This is a gemini capsule that allows users to upload their own mp3s (or oggs) to thier own private library (via Titan) and stream/download them via Gemini. A user's library is completely private. Nobody else can see the library, and songs are only streamable by the user that uploaded that song.
122 |
123 | In order to save space, AuraGem Music deduplicates songs by taking the hash of the audio contents. This is only done when the songs of multiple users are the *exact* same, and is done on upload of a song. Deduplication also has the benefit of lowering a user's quota. If the exact same song is in multiple users' libraries, the sum of the quotas for that song for each user adds up to 1. This is because the song is only stored once on the server. The quota is spread evenly between each of the users that have uploaded the song. The more users, the less quota each user has for that one song. You can find out more about how the quota system works below:
... | ...
145 | }
146 | openFile, err := os.Open(filepath.Join(musicDirectory, file.Filename))
147 | if err != nil {
148 | panic(err)
149 | }
+ 167 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# Random Music File\n"})
+ 168 | request.SetNoLanguage()
+ 169 | if request.ScrollMetadataRequested {
+ 170 | request.SendAbstract("audio/mpeg")
+ 171 | return
+ 172 | }
150 | request.Stream("audio/mpeg", openFile) // TODO: Use mimetype from db
151 | openFile.Close()
152 | return
153 | }
154 | }
... | ...
157 | s.AddRoute("/music/upload", func(request sis.Request) {
158 | if request.UserCert == nil {
159 | request.RequestClientCert("Please enable a certificate.")
160 | return
161 | }
- 162 | titanHost := "titan://auragem.letz.dev/"
- 163 | if request.Hostname() == "192.168.0.60" {
- 164 | titanHost = "titan://192.168.0.60/"
- 165 | } else if request.Hostname() == "auragem.ddns.net" {
- 166 | titanHost = "titan://auragem.ddns.net/"
+ 185 | uploadLink := ""
+ 186 | uploadMethod := ""
+ 187 | if request.Type == sis.ServerType_Gemini || request.Type == sis.ServerType_Scroll {
+ 188 | titanHost := "titan://auragem.letz.dev/"
+ 189 | if request.Hostname() == "192.168.0.60" {
+ 190 | titanHost = "titan://192.168.0.60/"
+ 191 | } else if request.Hostname() == "auragem.ddns.net" {
+ 192 | titanHost = "titan://auragem.ddns.net/"
+ 193 | }
+ 194 | uploadLink = "=> " + titanHost + "/music/upload"
+ 195 | uploadMethod = "Titan"
167 | }
168 |
... | ...
164 | }
165 |
- 169 | request.Gemini(fmt.Sprintf(`# Upload File with Titan
+ 198 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# Upload File with " + uploadMethod + "\n"})
+ 199 | if request.ScrollMetadataRequested {
+ 200 | request.SendAbstract("")
+ 201 | return
+ 202 | }
+ 203 |
+ 204 | request.Gemini(fmt.Sprintf(`# Upload File with %s
+ 205 |
+ 206 | Upload an mp3 music file to this page with %s. It will then be automatically added to your library. Please make sure that the metadata tags on the mp3 are correct and filled in before uploading, especially the Title, AlbumArtist, and Album tags.
170 |
... | ...
166 |
- 171 | Upload an mp3 music file to this page with Titan. It will then be automatically added to your library. Please make sure that the metadata tags on the mp3 are correct and filled in before uploading, especially the Title, AlbumArtist, and Album tags.
+ 208 | %s Upload
172 |
... | ...
168 |
- 173 | => %s/music/upload Upload
174 | => gemini://transjovian.org/titan About Titan
... | ...
170 | => gemini://transjovian.org/titan About Titan
- 175 | `, titanHost))
+ 211 | `, uploadMethod, uploadMethod, uploadLink))
176 | })
177 |
178 | s.AddUploadRoute("/music/upload", func(request sis.Request) {
179 | cert := request.UserCert
180 | if cert == nil {
... | ...
185 | if !isRegistered {
186 | //return c.Gemini(registerNotification)
187 | request.TemporaryFailure("You must be registered first before you can upload.")
188 | return
189 | } else {
- 190 | file, read_err := request.GetUploadData()
- 191 | if read_err != nil {
- 192 | return //read_err
- 193 | }
- 194 |
- 195 | // First, check mimetype
+ 226 | // First, check mimetype if using Titan
196 | mimetype := request.DataMime
... | ...
192 | mimetype := request.DataMime
- 197 | if !strings.HasPrefix(mimetype, "audio/mpeg") && !strings.HasPrefix(mimetype, "audio/mp3") {
+ 228 | if (request.Type == sis.ServerType_Gemini || request.Type == sis.ServerType_Scroll) && !strings.HasPrefix(mimetype, "audio/mpeg") && !strings.HasPrefix(mimetype, "audio/mp3") {
198 | request.TemporaryFailure("Only mp3 audio files are allowed.")
... | ...
194 | request.TemporaryFailure("Only mp3 audio files are allowed.")
+ 230 | return
+ 231 | } else if !(request.Type == sis.ServerType_Gemini || request.Type == sis.ServerType_Scroll) {
+ 232 | request.TemporaryFailure("Upload only supported via Titan.")
199 | return
200 | }
201 |
... | ...
197 | return
198 | }
199 |
+ 236 | // Check the size
+ 237 | if request.DataSize > 15*1024*1024 { // Max of 15 MB
+ 238 | request.TemporaryFailure("File too large. Max size is 15 MiB.")
+ 239 | return
+ 240 | }
+ 241 |
+ 242 | file, read_err := request.GetUploadData()
+ 243 | if read_err != nil {
+ 244 | return //read_err
+ 245 | }
+ 246 |
202 | // TODO: Check if data folder is mounted properly before doing anything?
203 |
204 | // Then, get hash of file
205 | hash, _ := tag.Sum(bytes.NewReader(file))
206 | fmt.Printf("Hash: %s\n", hash)
... | ...
277 |
278 | // Add to user library
279 | AddFileToUserLibrary(conn, musicFile.Id, user.Id, false)
280 | }
281 |
- 282 | request.Redirect("gemini://%s/music/", request.Hostname())
+ 327 | request.Redirect("%s%s/music/", request.Server.Scheme(), request.Hostname())
283 | return
284 | //return c.NoContent(gig.StatusRedirectTemporary, "gemini://%s/music/", c.URL().Host)
285 | }
286 | }
287 | })
... | ...
305 | if !exists {
306 | request.NotFound("File not found.")
307 | return
308 | }
309 |
+ 355 | abstract := "# " + file.Title + "\n"
+ 356 | if file.Album != "" {
+ 357 | abstract += "Album: " + file.Album + "\n"
+ 358 | }
+ 359 | if file.Tracknumber != 0 {
+ 360 | abstract += "Track: " + strconv.Itoa(file.Tracknumber) + "\n"
+ 361 | }
+ 362 | if file.Discnumber != 0 && file.Discnumber != 1 {
+ 363 | abstract += "Disk: " + strconv.Itoa(file.Discnumber) + "\n"
+ 364 | }
+ 365 | if file.Albumartist != "" {
+ 366 | abstract += "Album Artist: " + file.Albumartist + "\n"
+ 367 | }
+ 368 | if file.Composer != "" {
+ 369 | abstract += "Composer: " + file.Composer + "\n"
+ 370 | }
+ 371 | if file.Genre != "" {
+ 372 | abstract += "Genre: " + file.Genre + "\n"
+ 373 | }
+ 374 | if file.Releaseyear != 0 {
+ 375 | abstract += "Release Year: " + strconv.Itoa(file.Releaseyear) + "\n"
+ 376 | }
+ 377 | abstract += "Kbps: " + strconv.Itoa(int(file.CbrKbps)) + "\n"
+ 378 | if file.Attribution != "" {
+ 379 | abstract += "\nAttribution:\n" + file.Attribution + "\n"
+ 380 | }
+ 381 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: file.Artist, Abstract: abstract})
+ 382 | request.SetNoLanguage()
+ 383 | if request.ScrollMetadataRequested {
+ 384 | request.SendAbstract("audio/mpeg")
+ 385 | return
+ 386 | }
+ 387 |
310 | StreamFile(request, file)
311 | return
312 | //q := `SELECT COUNT(*) FROM uploads INNER JOIN library ON uploads.fileid=library.id WHERE uploads.memberid=? AND library.filename=?`
313 | }
314 | }
... | ...
325 | request.Gemini(registerNotification)
326 | return
327 | } else {
328 | albums := GetAlbumsInUserLibrary(conn, user.Id)
329 |
+ 408 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: user.Date_joined, Abstract: "# AuraGem Music - " + user.Username + "\n## Albums\n"})
+ 409 | if request.ScrollMetadataRequested {
+ 410 | request.SendAbstract("")
+ 411 | return
+ 412 | }
+ 413 |
330 | var builder strings.Builder
331 | for _, album := range albums {
332 | fmt.Fprintf(&builder, "=> /music/artist/%s/%s %s - %s\n", url.PathEscape(album.Albumartist), url.PathEscape(album.Album), album.Album, album.Albumartist)
333 | }
334 |
... | ...
354 | if !isRegistered {
355 | request.Gemini(registerNotification)
356 | return
357 | } else {
358 | artists := GetArtistsInUserLibrary(conn, user.Id)
+ 443 |
+ 444 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: user.Date_joined, Abstract: "# AuraGem Music - " + user.Username + "\n## Artists\n"})
+ 445 | if request.ScrollMetadataRequested {
+ 446 | request.SendAbstract("")
+ 447 | return
+ 448 | }
359 |
360 | var builder strings.Builder
361 | for _, artist := range artists {
362 | fmt.Fprintf(&builder, "=> /music/artist/%s %s\n", url.PathEscape(artist), artist)
363 | }
... | ...
441 | user, isRegistered := GetUser(conn, request.UserCertHash_Gemini())
442 | if !isRegistered {
443 | request.Gemini(registerNotification)
444 | return
445 | } else {
+ 536 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music Shuffled Stream - " + user.Username + "\n"})
+ 537 | if request.ScrollMetadataRequested {
+ 538 | request.SendAbstract("audio/mpeg")
+ 539 | return
+ 540 | }
+ 541 |
446 | StreamRandomFiles(request, conn, user)
447 | return
448 | }
449 | }
450 | })
... | ...
597 | err := row.Scan(&numRows)
598 | if err != nil {
599 | panic(err)
600 | }
601 | if numRows < 1 {
+ 698 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music - Register User " + username + "\n"})
+ 699 | if request.ScrollMetadataRequested {
+ 700 | request.SendAbstract("")
+ 701 | return
+ 702 | }
+ 703 |
602 | // Certificate doesn't already exist - Register User
603 | zone, _ := time.Now().Zone()
604 | conn.ExecContext(context.Background(), "INSERT INTO members (certificate, username, language, timezone, is_staff, is_active, date_joined) VALUES (?, ?, ?, ?, ?, ?, ?)", certHash, username, "en-US", zone, false, true, time.Now())
605 |
606 | user, _ := GetUser(conn, certHash)
... | ...
659 |
660 | request.Gemini(fmt.Sprintf(template, user.Username, user.QuotaCount, userSongQuota, user.QuotaCount/float64(userSongQuota)*100, builder.String()))
661 | }
662 |
663 | func artistAlbums(request sis.Request, conn *sql.DB, user MusicUser, artist string) {
+ 766 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music - " + user.Username + "\n## Artist Albums: " + artist + "\n"})
+ 767 | if request.ScrollMetadataRequested {
+ 768 | request.SendAbstract("")
+ 769 | return
+ 770 | }
+ 771 |
664 | albums := GetAlbumsFromArtistInUserLibrary(conn, user.Id, artist)
665 |
666 | var builder strings.Builder
667 | for _, album := range albums {
668 | fmt.Fprintf(&builder, "=> /music/artist/%s/%s %s\n", url.PathEscape(album.Albumartist), url.PathEscape(album.Album), album.Album)
... | ...
678 | %s
679 | `, user.Username, artist, url.PathEscape(artist), builder.String()))
680 | }
681 |
682 | func albumSongs(request sis.Request, conn *sql.DB, user MusicUser, artist string, album string) {
+ 791 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music - " + user.Username + "\n## Album: " + album + " by " + artist + "\n"})
+ 792 | if request.ScrollMetadataRequested {
+ 793 | request.SendAbstract("")
+ 794 | return
+ 795 | }
+ 796 |
683 | musicFiles := GetFilesFromAlbumInUserLibrary(conn, user.Id, artist, album)
684 |
685 | var builder strings.Builder
686 | var albumartist string
687 | for i, file := range musicFiles {
... | ...
705 | %s
706 | `, user.Username, album, artist, url.PathEscape(albumartist), albumartist, url.PathEscape(albumartist), url.PathEscape(album), builder.String()))
707 | }
708 |
709 | func adminPage(request sis.Request, conn *sql.DB, user MusicUser) {
+ 824 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music - Admin\n"})
+ 825 | if request.ScrollMetadataRequested {
+ 826 | request.SendAbstract("")
+ 827 | return
+ 828 | }
+ 829 |
710 | var builder strings.Builder
711 | radioGenres := Admin_RadioGenreCounts(conn)
712 | for _, genre := range radioGenres {
713 | fmt.Fprintf(&builder, "=> /music/admin/genre?%s %s (%d)\n", url.QueryEscape(genre.Name), genre.Name, genre.Count)
714 | }
... | ...
736 |
737 | request.Gemini(fmt.Sprintf(template, globalQuotaCount, globalSongQuota, globalQuotaCount/globalSongQuota*100, avgUserQuotaCount, userSongQuota, avgUserQuotaCount/float64(userSongQuota)*100, userCount, artistCount, albumCount, builder.String()))
738 | }
739 |
740 | func adminGenrePage(request sis.Request, conn *sql.DB, user MusicUser, genre_string string) {
+ 861 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# AuraGem Music - Admin: Genre" + genre_string + "\n"})
+ 862 | if request.ScrollMetadataRequested {
+ 863 | request.SendAbstract("")
+ 864 | return
+ 865 | }
+ 866 |
741 | songsInGenre := GetFilesInGenre(conn, genre_string)
742 | var builder strings.Builder
743 | fmt.Fprintf(&builder, "```\n")
744 | for _, song := range songsInGenre {
745 | fmt.Fprintf(&builder, "%-25s %-25s\n", song.Title, song.Artist)
... | ...
752 | `, genre_string, builder.String()))
753 | }
754 |
755 | // Streams all songs in album in one streams
756 | func streamAlbumSongs(request sis.Request, conn *sql.DB, user MusicUser, artist string, album string) {
+ 883 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# Stream Album " + album + " by " + artist + "\n"})
+ 884 | if request.ScrollMetadataRequested {
+ 885 | request.SendAbstract("")
+ 886 | return
+ 887 | }
+ 888 |
757 | musicFiles := GetFilesFromAlbumInUserLibrary(conn, user.Id, artist, album)
758 | fmt.Printf("Music Files: %v\n", musicFiles)
759 |
760 | /*filenames := make([]string, 0, len(musicFiles))
761 | for _, file := range musicFiles {
... | ...
764 |
765 | StreamMultipleFiles(request, musicFiles)
766 | }
767 |
768 | func streamArtistSongs(request sis.Request, conn *sql.DB, user MusicUser, artist string) {
+ 901 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# Stream Songs by " + artist + "\n"})
+ 902 | if request.ScrollMetadataRequested {
+ 903 | request.SendAbstract("")
+ 904 | return
+ 905 | }
+ 906 |
769 | musicFiles := GetFilesFromArtistInUserLibrary(conn, user.Id, artist)
770 |
771 | /*filenames := make([]string, 0, len(musicFiles))
772 | for _, file := range musicFiles {
773 | filenames = append(filenames, file.Filename)
... | ...
777 | }
778 |
779 | // ----- Manage Library Functions -----
780 |
781 | func manageLibrary(request sis.Request, user MusicUser) {
+ 920 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: user.Date_joined, Abstract: "# Manage Library - " + user.Username + "\n"})
+ 921 | if request.ScrollMetadataRequested {
+ 922 | request.SendAbstract("")
+ 923 | return
+ 924 | }
+ 925 |
782 | request.Gemini(fmt.Sprintf(`# Manage Library - %s
783 |
784 | Choose what you want to do. These links will direct you to pages that will allow you to select songs out of your library for the action you selected.
785 |
786 | => /music/ Dashboard
... | ...
790 |
791 | `, user.Username))
792 | }
793 |
794 | func manageLibrary_deleteSelection(request sis.Request, conn *sql.DB, user MusicUser) {
+ 939 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# Manage Library: Delete Selection - " + user.Username + "\n"})
+ 940 | if request.ScrollMetadataRequested {
+ 941 | request.SendAbstract("")
+ 942 | return
+ 943 | }
+ 944 |
795 | // TODO: Add Pagination
796 | musicFiles := GetFilesInUserLibrary(conn, user.Id)
797 |
798 | var builder strings.Builder
799 | for _, file := range musicFiles {
... | ...
822 |
823 | func manageLibrary_deleteFile(request sis.Request, conn *sql.DB, user MusicUser, hash string) {
824 | file, exists := GetFileInUserLibrary_hash(conn, hash, user.Id)
825 | if !exists {
826 | request.TemporaryFailure("File not in user library.")
+ 977 | return
+ 978 | }
+ 979 |
+ 980 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Abstract: "# Manage Library: Delete File - " + user.Username + "\nDelete " + file.Title + " by " + file.Artist + " (" + file.Album + ")\n"})
+ 981 | if request.ScrollMetadataRequested {
+ 982 | request.SendAbstract("")
827 | return
828 | }
829 |
830 | RemoveFileFromUserLibrary(conn, file.Id, user.Id)
831 | request.Redirect("/music/manage/delete")
gemini/music/music_index.gmi (created)
+ 1 | # AuraGem Music + 2 | + 3 | Welcome to the new AuraGem Music Service, where you can upload a limited number of mp3s over Titan and listen to your private music library over Gemini. The service is also available over the Scroll Protocol. + 4 | + 5 | Note: Remember to make sure your certificate is selected on this page if you've already registered. + 6 | + 7 | In order to register, create and enable a client certificate and then head over to the Register Cert page: + 8 | + 9 | => /music/register Register Cert + 10 | => /music/quota How the Quota System Works + 11 | => gemini://transjovian.org/titan About Titan + 12 | + 13 | => /music/public_radio/ Public Radio + 14 | + 15 | ## Features + 16 | + 17 | * Upload MP3s + 18 | * Music organized by Artist and Album + 19 | * Stream a full album or a particular artist's songs + 20 | * "Shuffled stream" - infinite stream of random songs from user's private library + 21 | * Delete songs from library + 22 | + 23 | ## The Legality of AuraGem Music + 24 | + 25 | AuraGem Music is currently not a distribution platform. Instead, it hosts a user's personal collection of music for their own consumption. Therefore, the music a user uploads is only visible to that person and will not be distributed to others. Uploading music that the user has bought is legitimate. However, the user is solely responsible for any pirated content that they have uploaded. + 26 | + 27 | => scroll://auragem.letz.dev/music/ Via Scroll
gemini/music/music_index_scroll.scroll (created)
+ 1 | # AuraGem Music + 2 | + 3 | Welcome to the new AuraGem Music Service, where you can upload a limited number of mp3s over Titan and listen to your private music library over Scroll. This service is also available over Gemini+Titan. + 4 | + 5 | Note: Remember to make sure your certificate is selected on this page if you've already registered. + 6 | + 7 | In order to register, create and enable a client certificate and then head over to the Register Cert page: + 8 | + 9 | => /music/register Register Cert + 10 | => /music/quota How the Quota System Works + 11 | => gemini://transjovian.org/titan About Titan + 12 | + 13 | => /music/public_radio/ Public Radio + 14 | + 15 | ## Features + 16 | + 17 | * Upload MP3s + 18 | * Music organized by Artist and Album + 19 | * Stream a full album or a particular artist's songs + 20 | * "Shuffled stream" - infinite stream of random songs from user's private library + 21 | * Delete songs from library + 22 | + 23 | ## The Legality of AuraGem Music + 24 | + 25 | AuraGem Music is currently not a distribution platform. Instead, it hosts a user's personal collection of music for their own consumption. Therefore, the music a user uploads is only visible to that person and will not be distributed to others. Uploading music that the user has bought is legitimate. However, the user is solely responsible for any pirated content that they have uploaded. + 26 | + 27 | => gemini://auragem.letz.dev/music/ Via Gemini
gemini/music/radio.go
... | ...
164 | })
165 | s.AddRoute("/music/public_radio/", func(request sis.Request) {
166 | request.Redirect("/music/public_radio")
167 | })
168 | s.AddRoute("/music/public_radio", func(request sis.Request) {
+ 169 | creationDate, _ := time.ParseInLocation(time.RFC3339, "2023-09-19T00:00:00", time.Local)
+ 170 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2023-11-30T00:00:00", time.Local)
+ 171 | abstract := `# AuraGem Music: Public Radio
+ 172 |
+ 173 | This is AuraGem Music's public radio that plays public domain and royalty free music. All music is collected from sources like the Free Music Archive, archive.org, and Chosic, and stored on my server. This radio does not proxy from the web, unlike other radios over on Gopherspace.
+ 174 | `
+ 175 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: creationDate.UTC(), UpdateDate: updateDate.UTC(), Language: "en", Abstract: abstract})
+ 176 | if request.ScrollMetadataRequested {
+ 177 | request.SendAbstract("")
+ 178 | return
+ 179 | }
+ 180 |
169 | var builder strings.Builder
170 | for _, station := range radioStations {
171 | fmt.Fprintf(&builder, "=> /music/public_radio/%s/ %s Station\n", url.PathEscape(station.Name), station.Name)
172 | }
173 | request.Gemini(fmt.Sprintf(`# AuraGem Music: Public Radio
gemini/music/stations.go
... | ...
27 |
28 | go radioService(conn, radioBuffer, station)
29 | go fakeClient(radioBuffer, station)
30 |
31 | s.AddRoute("/music/public_radio/"+url.PathEscape(station.Name), func(request sis.Request) {
+ 32 | creationDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-14T18:07:00", time.Local)
+ 33 | creationDate = creationDate.UTC()
+ 34 | abstract := fmt.Sprintf("# AuraGem Public Radio - %s Station\n\n%s\nClients Connected: %d\n", station.Name, station.Description, radioBuffer.clientCount)
+ 35 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: creationDate, UpdateDate: creationDate, Language: "en", Abstract: abstract})
+ 36 | if request.ScrollMetadataRequested {
+ 37 | request.SendAbstract("")
+ 38 | return
+ 39 | }
+ 40 |
32 | currentTime := time.Now()
33 | radioGenre := GetRadioGenre(currentTime, station)
34 |
35 | attribution := ""
36 | if radioBuffer.currentMusicFile.Attribution != "" {
... | ...
153 | `
154 | request.Gemini(fmt.Sprintf(template, station.Name, station.Description, url.PathEscape(station.Name), url.PathEscape(station.Name), radioBuffer.clientCount, currentTime.Format("03:04 PM"), radioGenre, radioBuffer.currentMusicFile.Title, radioBuffer.currentMusicFile.Artist, attribution, scheduleBuilder.String()))
155 | })
156 |
157 | s.AddRoute("/music/public_radio/"+url.PathEscape(station.Name)+"/schedule_feed", func(request sis.Request) {
+ 167 | creationDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-14T18:07:00", time.Local)
+ 168 | creationDate = creationDate.UTC()
+ 169 | abstract := fmt.Sprintf("# AuraGem Public Radio - %s Station Schedule\n", station.Name)
+ 170 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: creationDate, UpdateDate: time.Now(), Language: "en", Abstract: abstract})
+ 171 | if request.ScrollMetadataRequested {
+ 172 | request.SendAbstract("")
+ 173 | return
+ 174 | }
+ 175 |
158 | currentTime := time.Now()
159 | current_wd := currentTime.Weekday()
160 | program := station.ProgramInfo[current_wd]
161 | episode := station.CurrentEpisode[program]
162 | var hour int64 = 0
... | ...
198 | })
199 | s.AddRoute("/music/stream/public_radio/"+url.PathEscape(station.Name), func(request sis.Request) {
200 | request.Redirect("/music/stream/public_radio/" + url.PathEscape(station.Name) + ".mp3")
201 | })
202 | s.AddRoute("/music/stream/public_radio/"+url.PathEscape(station.Name)+".mp3", func(request sis.Request) {
+ 221 | creationDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-14T18:07:00", time.Local)
+ 222 | creationDate = creationDate.UTC()
+ 223 | abstract := ""
+ 224 | if request.ScrollMetadataRequested {
+ 225 | currentTime := time.Now()
+ 226 | radioGenre := GetRadioGenre(currentTime, station)
+ 227 | attribution := ""
+ 228 | if radioBuffer.currentMusicFile.Attribution != "" {
+ 229 | attribution = "\n" + radioBuffer.currentMusicFile.Attribution
+ 230 | }
+ 231 |
+ 232 | abstract = fmt.Sprintf("# AuraGem Public Radio - %s Station\n\n%s\nClients Currently Connected to Station: %d\nCurrent Time and Genre: %s CST (%s)\nCurrent song playing: %s by %s\n%s", station.Name, station.Description, radioBuffer.clientCount, currentTime.Format("03:04 PM"), radioGenre, radioBuffer.currentMusicFile.Title, radioBuffer.currentMusicFile.Artist, attribution)
+ 233 | }
+ 234 |
+ 235 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: creationDate, UpdateDate: time.Now(), Language: "en", Abstract: abstract})
+ 236 | if request.ScrollMetadataRequested {
+ 237 | request.SendAbstract("audio/mpeg")
+ 238 | return
+ 239 | }
+ 240 |
203 | // Station streaming here
204 | // Add to client count
205 | radioBuffer.clientCount += 1
206 | (*totalClientsConnected) += 1
207 |
gemini/search/feedback.go
... | ...
30 | if request.GetParam("token") != token {
31 | request.TemporaryFailure("A token is required.")
32 | return
33 | }
34 |
+ 35 | if request.DataSize > 5*1024*1024 {
+ 36 | request.TemporaryFailure("Size too large.")
+ 37 | return
+ 38 | }
+ 39 |
35 | // TODO: Check that the mimetype is gemini or text file
36 |
37 | data, err := request.GetUploadData()
38 | if err != nil {
39 | return //err
... | ...
61 |
62 | err = request.Server.FS().WriteFile("searchfeedback.gmi", data, 0600)
63 | if err != nil {
64 | return //err
65 | }
- 66 | request.Redirect("gemini://%s:%s/search/feedback.gmi", request.Server.Hostname(), request.Server.Port())
+ 71 | request.Redirect("%s%s:%s/search/feedback.gmi", request.Server.Scheme(), request.Server.Hostname(), request.Server.Port())
67 | return
68 | } else {
69 | fileData, err := request.Server.FS().ReadFile("searchfeedback.gmi")
70 | if err != nil {
71 | request.TemporaryFailure(err.Error())
gemini/search/search.go
... | ...
-1 | package search
0 |
1 | // TODO: Add Favicons text to database and have crawler look for favicon.txt file
- 4 | // TODO: Also add language to pages table in database
- 5 | // TODO: Line count of pages in database
6 | // TOOD: track Hashtags and Mentions (mentions can start with @ or ~)
7 |
8 | import (
9 | "context"
10 | "database/sql"
... | ...
137 | // Search Engine Handles
138 | s.AddRoute("/search", func(request sis.Request) {
139 | request.Redirect("/search/")
140 | })
141 | // TODO: Removed Tag Index (=> /search/tags 🏷️ Tag Index)
+ 140 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2021-07-01T00:00:00", time.Local)
+ 141 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-13T00:00:00", time.Local)
142 | s.AddRoute("/search/", func(request sis.Request) {
... | ...
138 | s.AddRoute("/search/", func(request sis.Request) {
+ 143 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search\n"})
+ 144 | if request.ScrollMetadataRequested {
+ 145 | request.SendAbstract("")
+ 146 | return
+ 147 | }
+ 148 |
143 | request.Gemini("# AuraGem Search\n\n")
144 | request.PromptLine("/search/s/", "🔍 Search")
145 | request.Gemini(`=> /search/random/ 🎲 Goto Random Capsule
146 | => /search/backlinks/ Check Backlinks
147 |
... | ...
192 | //
193 | // => https://www.patreon.com/krixano Patreon
194 | })
195 |
196 | s.AddRoute("/search/configure_default", func(request sis.Request) {
+ 203 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# Configure Default Search Engine in Lagrange\n"})
+ 204 | if request.ScrollMetadataRequested {
+ 205 | request.SendAbstract("")
+ 206 | return
+ 207 | }
+ 208 |
197 | request.Gemini(`# Configure Default Search Engine in Lagrange
198 |
199 | 1. Go to File -> Preferences -> General
200 | 2. Paste the following link into the Search URL field:
201 | > gemini://auragem.letz.dev/search/s
... | ...
201 | > gemini://auragem.letz.dev/search/s
202 | `)
203 | })
204 |
205 | s.AddRoute("/search/features", func(request sis.Request) {
+ 218 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search Features\n"})
+ 219 | if request.ScrollMetadataRequested {
+ 220 | request.SendAbstract("")
+ 221 | return
+ 222 | }
+ 223 |
206 | request.Gemini(`# AuraGem Search Features
207 |
208 | ## Current State of Features
209 | * Full Text Search of page and file metadata, with Stemming, because apparently other search engines think it's important and unique to advertise one of the most common features in searching systems, lol.
210 | * Complex search queries using AND, OR, and NOT operators, as well as grouping using parentheses and quotes for multiword search terms. By default, if you do not use any of these operators, search terms are combined using OR, much like you would expect from web search engines. However, searches that have all the terms provided will still be ranked higher than searches with just one or a portion of the terms provided.
... | ...
279 | var totalSizeCache float64 = -1
280 | var totalSizeTextCache float64 = -1
281 | var lastCacheTime time.Time
282 | s.AddRoute("/search/stats", func(request sis.Request) {
283 | currentTime := time.Now()
+ 302 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: currentTime, Abstract: "# AuraGem Search Stats\n"})
+ 303 | if request.ScrollMetadataRequested {
+ 304 | request.SendAbstract("")
+ 305 | return
+ 306 | }
+ 307 |
284 | if totalSizeCache == -1 || lastCacheTime.Add(refreshCacheEvery).Before(currentTime) {
285 | row := conn.QueryRowContext(context.Background(), "SELECT COUNT(*), MAX(LAST_SUCCESSFUL_VISIT), SUM(SIZE) FROM pages")
286 | row.Scan(&pagesCountCache, &lastCrawlCache, &totalSizeCache)
287 | // Convert totalSize to GB
288 | lastCacheTime = currentTime
... | ...
285 | row := conn.QueryRowContext(context.Background(), "SELECT COUNT(*), MAX(LAST_SUCCESSFUL_VISIT), SUM(SIZE) FROM pages")
286 | row.Scan(&pagesCountCache, &lastCrawlCache, &totalSizeCache)
287 | // Convert totalSize to GB
288 | lastCacheTime = currentTime
289 | }
+ 314 |
290 | totalSize := totalSizeCache
291 | totalSize /= 1024 // Bytes to KB
292 | totalSize /= 1024 // KB to MB
293 | totalSize /= 1024 // MB to GB
294 |
... | ...
346 | return
347 | } else if query == "" {
348 | request.RequestInput("Capsule:")
349 | return
350 | } else {
+ 376 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Add Capsule to Index\n"})
+ 377 | if request.ScrollMetadataRequested {
+ 378 | request.SendAbstract("")
+ 379 | return
+ 380 | }
+ 381 |
351 | queryUrl, parse_err := url.Parse(query)
352 | if parse_err != nil {
353 | request.Redirect("/search/add_capsule")
354 | return
355 | }
... | ...
379 | return
380 | } else if query == "" {
381 | request.RequestInput("Gemini URL:")
382 | return
383 | } else {
+ 415 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Backlinks\n"})
+ 416 | if request.ScrollMetadataRequested {
+ 417 | request.SendAbstract("")
+ 418 | return
+ 419 | }
+ 420 |
384 | // Check that gemini url in query string is correct
385 | queryUrl, parse_err := url.Parse(query)
386 | if parse_err != nil {
387 | request.Redirect("/search/backlinks")
388 | return
... | ...
409 | return
410 | } else if query == "" {
411 | request.RequestInput("Search Query:")
412 | return
413 | } else {
+ 451 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - '" + query + "'\n"})
+ 452 | if request.ScrollMetadataRequested {
+ 453 | request.SendAbstract("")
+ 454 | return
+ 455 | }
+ 456 |
414 | // Page 1
415 | handleSearch(request, conn, query, 1, false)
416 | return
417 | }
418 | })
... | ...
431 | return
432 | } else if query == "" {
433 | request.RequestInput("Search Query:")
434 | return
435 | } else {
+ 479 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - '" + query + "' Page " + pageStr + "\n"})
+ 480 | if request.ScrollMetadataRequested {
+ 481 | request.SendAbstract("")
+ 482 | return
+ 483 | }
+ 484 |
436 | handleSearch(request, conn, query, page, false)
437 | return
438 | }
439 | })
440 |
... | ...
474 | return
475 | }
476 | })
477 |
478 | s.AddRoute("/search/recent", func(request sis.Request) {
+ 528 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - 50 Most Recently Indexed\n"})
+ 529 | if request.ScrollMetadataRequested {
+ 530 | request.SendAbstract("")
+ 531 | return
+ 532 | }
+ 533 |
479 | pages := getRecent(conn)
480 |
481 | var builder strings.Builder
482 | buildPageResults(&builder, pages, false, false)
483 |
... | ...
489 | %s
490 | `, builder.String()))
491 | })
492 |
493 | s.AddRoute("/search/capsules", func(request sis.Request) {
+ 549 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - List of Capsules\n"})
+ 550 | if request.ScrollMetadataRequested {
+ 551 | request.SendAbstract("")
+ 552 | return
+ 553 | }
+ 554 |
494 | capsules := getCapsules(conn)
495 |
496 | var builder strings.Builder
497 | for _, capsule := range capsules {
498 | if capsule.Title == "" {
... | ...
510 | %s
511 | `, builder.String()))
512 | })
513 |
514 | s.AddRoute("/search/tags", func(request sis.Request) {
+ 576 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Tag Index\n"})
+ 577 | if request.ScrollMetadataRequested {
+ 578 | request.SendAbstract("")
+ 579 | return
+ 580 | }
+ 581 |
515 | tags := getTags(conn)
516 |
517 | var builder strings.Builder
518 | for _, tag := range tags {
519 | fmt.Fprintf(&builder, "=> /search/tag/%s %s (%d)\n", url.PathEscape(tag.Name), tag.Name, tag.Count)
... | ...
542 | %s
543 | `, request.GetParam("name"), builder.String()))
544 | })
545 |
546 | s.AddRoute("/search/feeds", func(request sis.Request) {
+ 614 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Feeds\n"})
+ 615 | if request.ScrollMetadataRequested {
+ 616 | request.SendAbstract("")
+ 617 | return
+ 618 | }
+ 619 |
547 | pages := getFeeds(conn)
548 |
549 | var builder strings.Builder
550 | buildPageResults(&builder, pages, false, false)
551 |
... | ...
556 |
557 | %s
558 | `, builder.String()))
559 | })
560 |
- 561 | s.AddRoute("/search/test", func(request sis.Request) {
- 562 | request.Redirect("/search/yearposts")
- 563 | })
564 | s.AddRoute("/search/yearposts", func(request sis.Request) {
... | ...
560 | s.AddRoute("/search/yearposts", func(request sis.Request) {
+ 635 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Posts From The Past Year\n"})
+ 636 | if request.ScrollMetadataRequested {
+ 637 | request.SendAbstract("")
+ 638 | return
+ 639 | }
+ 640 |
565 | page := 1
566 | results := 40
567 | skip := (page - 1) * results
568 |
569 | pages, totalResultsCount := getPagesWithPublishDateFromLastYear(conn, results, skip)
... | ...
596 | `, builder.String()))
597 | })
598 |
599 | s.AddRoute("/search/yearposts/:page", func(request sis.Request) {
600 | pageStr := request.GetParam("page")
+ 677 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Posts From The Past Year, Page " + pageStr + "\n"})
+ 678 | if request.ScrollMetadataRequested {
+ 679 | request.SendAbstract("")
+ 680 | return
+ 681 | }
+ 682 |
601 | page, err := strconv.Atoi(pageStr)
602 | if err != nil {
603 | request.BadRequest("Couldn't parse int.")
604 | return
605 | }
... | ...
636 | %s
637 | `, builder.String()))
638 | })
639 |
640 | s.AddRoute("/search/audio", func(request sis.Request) {
- 641 | /* pageStr := c.Param("page")
- 642 | page_int, parse_err := strconv.ParseInt(pageStr, 10, 64)
- 643 | if parse_err != nil {
- 644 | return c.NoContent(gig.StatusBadRequest, "Page Number Error")
- 645 | }*/
+ 723 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Audio Files\n"})
+ 724 | if request.ScrollMetadataRequested {
+ 725 | request.SendAbstract("")
+ 726 | return
+ 727 | }
+ 728 |
646 | pages, _, _ := getAudioFiles(conn, 1)
647 |
648 | var builder strings.Builder
649 | buildPageResults(&builder, pages, false, false)
650 | /*for _, page := range pages {
... | ...
677 | page_int, parse_err := strconv.ParseInt(pageStr, 10, 64)
678 | if parse_err != nil {
679 | request.BadRequest("Page Number Error")
680 | return
681 | }
+ 765 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Audio Files, Page " + pageStr + "\n"})
+ 766 | if request.ScrollMetadataRequested {
+ 767 | request.SendAbstract("")
+ 768 | return
+ 769 | }
+ 770 |
682 | pages, _, hasNextPage := getAudioFiles(conn, page_int)
683 |
684 | var builder strings.Builder
685 | buildPageResults(&builder, pages, false, false)
686 |
... | ...
737 | return
738 | }
739 | })
740 |
741 | s.AddRoute("/search/images", func(request sis.Request) {
+ 831 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Image Files\n"})
+ 832 | if request.ScrollMetadataRequested {
+ 833 | request.SendAbstract("")
+ 834 | return
+ 835 | }
+ 836 |
742 | pages, _, _ := getImageFiles(conn, 1)
743 |
744 | var builder strings.Builder
745 | buildPageResults(&builder, pages, false, false)
746 | /*for _, page := range pages {
... | ...
773 | page_int, parse_err := strconv.ParseInt(pageStr, 10, 64)
774 | if parse_err != nil {
775 | request.BadRequest("Page Number Error")
776 | return
777 | }
+ 873 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Image Files, Page " + pageStr + "\n"})
+ 874 | if request.ScrollMetadataRequested {
+ 875 | request.SendAbstract("")
+ 876 | return
+ 877 | }
+ 878 |
778 | pages, _, hasNextPage := getImageFiles(conn, page_int)
779 | if len(pages) == 0 {
780 | request.NotFound("Page not found.")
781 | return
782 | }
... | ...
801 | %s
802 | `, builder.String()))
803 | })
804 |
805 | s.AddRoute("/search/twtxt", func(request sis.Request) {
+ 907 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Twtxt Files\n"})
+ 908 | if request.ScrollMetadataRequested {
+ 909 | request.SendAbstract("")
+ 910 | return
+ 911 | }
+ 912 |
806 | pages := getTwtxtFiles(conn)
807 | if len(pages) == 0 {
808 | request.NotFound("Page not found.")
809 | return
810 | }
... | ...
827 | %s
828 | `, builder.String()))
829 | })
830 |
831 | s.AddRoute("/search/security", func(request sis.Request) {
+ 939 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed Security.txt Files\n"})
+ 940 | if request.ScrollMetadataRequested {
+ 941 | request.SendAbstract("")
+ 942 | return
+ 943 | }
+ 944 |
832 | pages := getSecurityTxtFiles(conn)
833 | if len(pages) == 0 {
834 | request.NotFound("Page not found.")
835 | return
836 | }
... | ...
857 | query, err := request.Query()
858 | if err != nil {
859 | request.TemporaryFailure(err.Error())
860 | return
861 | } else if query == "" {
+ 975 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Mimetypes\n"})
+ 976 | if request.ScrollMetadataRequested {
+ 977 | request.SendAbstract("")
+ 978 | return
+ 979 | }
+ 980 |
862 | mimetypesList := getMimetypes(conn)
863 | var mimetypes strings.Builder
864 | for _, item := range mimetypesList {
865 | fmt.Fprintf(&mimetypes, "=> /search/s/?%s %s (%d)\n", url.QueryEscape("CONTENTTYPE:("+item.mimetype+")"), item.mimetype, item.count)
866 | }
... | ...
871 | => /search/s/ Search
872 |
873 | %s
874 | `, mimetypes.String()))
875 | } else {
+ 995 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Abstract: "# AuraGem Search - Indexed of Mimetype '" + query + "'\n"})
+ 996 | if request.ScrollMetadataRequested {
+ 997 | request.SendAbstract("")
+ 998 | return
+ 999 | }
+ 1000 |
876 | pages := getMimetypeFiles(conn, query)
877 | if len(pages) == 0 {
878 | request.NotFound("Page not found.")
879 | return
880 | }
gemini/starwars/queries.go (created)
+ 1 | package starwars
+ 2 |
+ 3 | import (
+ 4 | "context"
+ 5 | "database/sql"
+ 6 | "time"
+ 7 | )
+ 8 |
+ 9 | func GetMovies(conn *sql.DB, timeline bool) ([]Movie, time.Time) {
+ 10 | lastUpdate := time.Time{}
+ 11 | var movies []Movie
+ 12 | var q string
+ 13 | if timeline {
+ 14 | q = `SELECT r.ID, r."NAME", r.TIMELINEDATE, r.PUBLICATIONDATE, r.PRODUCTIONCOMPANY, r.DISTRIBUTOR, r.DATE_ADDED FROM MOVIES r order by timelinedate asc`
+ 15 | } else {
+ 16 | q = `SELECT r.ID, r."NAME", r.TIMELINEDATE, r.PUBLICATIONDATE, r.PRODUCTIONCOMPANY, r.DISTRIBUTOR, r.DATE_ADDED FROM MOVIES r order by publicationdate asc`
+ 17 | }
+ 18 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 19 | if rows_err == nil {
+ 20 | defer rows.Close()
+ 21 | for rows.Next() {
+ 22 | var movie Movie
+ 23 | scan_err := rows.Scan(&movie.Id, &movie.Name, &movie.TimelineDate, &movie.PublicationDate, &movie.ProductionCompany, &movie.Distributor, &movie.Date_added)
+ 24 | if scan_err == nil {
+ 25 | if lastUpdate.Before(movie.Date_added) {
+ 26 | lastUpdate = movie.Date_added
+ 27 | }
+ 28 | movies = append(movies, movie)
+ 29 | }
+ 30 | }
+ 31 | }
+ 32 |
+ 33 | return movies, lastUpdate
+ 34 | }
+ 35 |
+ 36 | func GetShows(conn *sql.DB) ([]TVShow, time.Time) {
+ 37 | lastUpdate := time.Time{}
+ 38 | var shows []TVShow
+ 39 | q := `SELECT r.ID, r."NAME", r.DATE_ADDED FROM TVSHOWS r`
+ 40 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 41 | if rows_err == nil {
+ 42 | defer rows.Close()
+ 43 | for rows.Next() {
+ 44 | var show TVShow
+ 45 | scan_err := rows.Scan(&show.Id, &show.Name, &show.Date_added)
+ 46 | if scan_err == nil {
+ 47 | if lastUpdate.Before(show.Date_added) {
+ 48 | lastUpdate = show.Date_added
+ 49 | }
+ 50 | shows = append(shows, show)
+ 51 | }
+ 52 | }
+ 53 | }
+ 54 |
+ 55 | return shows, lastUpdate
+ 56 | }
+ 57 |
+ 58 | func GetComicSeries_Full(conn *sql.DB) ([]ComicSeries, time.Time) {
+ 59 | lastUpdate := time.Time{}
+ 60 | var series []ComicSeries
+ 61 | q := `SELECT r.ID, r."NAME", r.MINISERIES, r.STARTYEAR, r.DATE_ADDED, (SELECT COUNT(*) FROM COMICTPBS WHERE COMICTPBS.COMICSERIESID=r.ID) as tpbcount, (SELECT COUNT(*) FROM COMICISSUES WHERE COMICISSUES.COMICSERIESID=r.ID AND COMICISSUES.ANNUAL=false) as issuecount FROM COMICSERIES r WHERE r.miniseries=false`
+ 62 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 63 | if rows_err == nil {
+ 64 | defer rows.Close()
+ 65 | for rows.Next() {
+ 66 | var serie ComicSeries
+ 67 | scan_err := rows.Scan(&serie.Id, &serie.Name, &serie.Miniseries, &serie.StartYear, &serie.Date_added, &serie.TPBCount, &serie.IssueCount)
+ 68 | if scan_err == nil {
+ 69 | if lastUpdate.Before(serie.Date_added) {
+ 70 | lastUpdate = serie.Date_added
+ 71 | }
+ 72 | series = append(series, serie)
+ 73 | }
+ 74 | }
+ 75 | }
+ 76 |
+ 77 | return series, lastUpdate
+ 78 | }
+ 79 |
+ 80 | func GetComicCrossovers(conn *sql.DB, timeline bool) ([]ComicTPB, time.Time) {
+ 81 | lastUpdate := time.Time{}
+ 82 | var tpbs []ComicTPB
+ 83 | var q string
+ 84 | if timeline {
+ 85 | q = `SELECT r.ID, r."NAME", r.CROSSOVER, r.TIMELINEDATE, r.PUBLICATIONDATE, r.DATE_ADDED FROM COMICTPBS r WHERE r.CROSSOVER=true order by timelinedate ASC`
+ 86 | } else {
+ 87 | q = `SELECT r.ID, r."NAME", r.CROSSOVER, r.TIMELINEDATE, r.PUBLICATIONDATE, r.DATE_ADDED FROM COMICTPBS r WHERE r.CROSSOVER=true order by publicationdate ASC`
+ 88 | }
+ 89 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 90 | if rows_err == nil {
+ 91 | defer rows.Close()
+ 92 | for rows.Next() {
+ 93 | var tpb ComicTPB
+ 94 | scan_err := rows.Scan(&tpb.Id, &tpb.Name, &tpb.Crossover, &tpb.TimelineDate, &tpb.PublicationDate, &tpb.Date_added)
+ 95 | if scan_err == nil {
+ 96 | if lastUpdate.Before(tpb.Date_added) {
+ 97 | lastUpdate = tpb.Date_added
+ 98 | }
+ 99 | tpbs = append(tpbs, tpb)
+ 100 | } else {
+ 101 | panic(scan_err)
+ 102 | }
+ 103 | }
+ 104 | } else {
+ 105 | panic(rows_err)
+ 106 | }
+ 107 |
+ 108 | return tpbs, lastUpdate
+ 109 | }
+ 110 |
+ 111 | func GetComicSeries_Miniseries(conn *sql.DB) ([]ComicSeries, time.Time) {
+ 112 | lastUpdate := time.Time{}
+ 113 | var series []ComicSeries
+ 114 | q := `SELECT r.ID, r."NAME", r.MINISERIES, r.STARTYEAR, r.DATE_ADDED, (SELECT COUNT(*) FROM COMICTPBS WHERE COMICTPBS.COMICSERIESID=r.ID) as tpbcount, (SELECT COUNT(*) FROM COMICISSUES WHERE COMICISSUES.COMICSERIESID=r.ID AND COMICISSUES.ANNUAL=false) as issuecount FROM COMICSERIES r WHERE r.miniseries=true`
+ 115 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 116 | if rows_err == nil {
+ 117 | defer rows.Close()
+ 118 | for rows.Next() {
+ 119 | var serie ComicSeries
+ 120 | scan_err := rows.Scan(&serie.Id, &serie.Name, &serie.Miniseries, &serie.StartYear, &serie.Date_added, &serie.TPBCount, &serie.IssueCount)
+ 121 | if scan_err == nil {
+ 122 | if lastUpdate.Before(serie.Date_added) {
+ 123 | lastUpdate = serie.Date_added
+ 124 | }
+ 125 | series = append(series, serie)
+ 126 | }
+ 127 | }
+ 128 | }
+ 129 |
+ 130 | return series, lastUpdate
+ 131 | }
+ 132 |
+ 133 | func GetTPBs(conn *sql.DB, timeline bool) ([]ComicTPB, time.Time) {
+ 134 | lastUpdate := time.Time{}
+ 135 | var tpbs []ComicTPB
+ 136 | var q string
+ 137 | if timeline {
+ 138 | q = `SELECT r.ID, r.VOLUME, r."NAME", r.CROSSOVER, r.TIMELINEDATE, r.PUBLICATIONDATE, r.DATE_ADDED, comicseries.ID, comicseries."NAME", comicseries.MINISERIES, comicseries.STARTYEAR, comicseries.DATE_ADDED, (SELECT COUNT(*) FROM COMICISSUES WHERE COMICISSUES.COMICTPBID=r.ID) as issuecount FROM COMICTPBS r LEFT JOIN COMICSERIES ON COMICSERIES.ID=r.COMICSERIESID order by timelinedate ASC`
+ 139 | } else {
+ 140 | q = `SELECT r.ID, r.VOLUME, r."NAME", r.CROSSOVER, r.TIMELINEDATE, r.PUBLICATIONDATE, r.DATE_ADDED, comicseries.ID, comicseries."NAME", comicseries.MINISERIES, comicseries.STARTYEAR, comicseries.DATE_ADDED, (SELECT COUNT(*) FROM COMICISSUES WHERE COMICISSUES.COMICTPBID=r.ID) as issuecount FROM COMICTPBS r LEFT JOIN COMICSERIES ON COMICSERIES.ID=r.COMICSERIESID order by publicationdate ASC`
+ 141 | }
+ 142 | rows, rows_err := conn.QueryContext(context.Background(), q) // TODO: ComicSeriesId
+ 143 | if rows_err == nil {
+ 144 | defer rows.Close()
+ 145 | for rows.Next() {
+ 146 | var tpb ComicTPB
+ 147 |
+ 148 | var volume interface{}
+ 149 | var timelineDate interface{}
+ 150 | var series comicSeriesNullable
+ 151 | scan_err := rows.Scan(&tpb.Id, &volume, &tpb.Name, &tpb.Crossover, &timelineDate, &tpb.PublicationDate, &tpb.Date_added, &series.Id, &series.Name, &series.Miniseries, &series.StartYear, &series.Date_added, &tpb.IssueCount)
+ 152 | if scan_err == nil {
+ 153 | if volume != nil {
+ 154 | tpb.Volume = int(volume.(int32))
+ 155 | }
+ 156 | if timelineDate != nil {
+ 157 | tpb.TimelineDate = int(timelineDate.(int32))
+ 158 | }
+ 159 | if lastUpdate.Before(tpb.Date_added) {
+ 160 | lastUpdate = tpb.Date_added
+ 161 | }
+ 162 | tpb.Series = comicSeriesScan(series)
+ 163 |
+ 164 | tpbs = append(tpbs, tpb)
+ 165 | } else {
+ 166 | panic(scan_err)
+ 167 | }
+ 168 | }
+ 169 | } else {
+ 170 | panic(rows_err)
+ 171 | }
+ 172 |
+ 173 | return tpbs, lastUpdate
+ 174 | }
+ 175 |
+ 176 | func GetComicIssues(conn *sql.DB, timeline bool) ([]ComicIssue, time.Time) {
+ 177 | lastUpdate := time.Time{}
+ 178 | var issues []ComicIssue
+ 179 | var q string
+ 180 | if timeline {
+ 181 | q = `SELECT r.ID, r."NUMBER", r."NAME", r.ANNUAL, r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED, comicseries.ID, comicseries."NAME", comicseries.MINISERIES, comicseries.STARTYEAR, comicseries.DATE_ADDED FROM COMICISSUES r LEFT JOIN COMICSERIES ON COMICSERIES.ID=r.COMICSERIESID order by r.TIMELINEDATE ASC, r.NUMBER ASC`
+ 182 | } else {
+ 183 | q = `SELECT r.ID, r."NUMBER", r."NAME", r.ANNUAL,r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED, comicseries.ID, comicseries."NAME", comicseries.MINISERIES, comicseries.STARTYEAR, comicseries.DATE_ADDED FROM COMICISSUES r LEFT JOIN COMICSERIES ON COMICSERIES.ID=r.COMICSERIESID order by r.PUBLICATIONDATE ASC, r.NUMBER ASC`
+ 184 | }
+ 185 | rows, rows_err := conn.QueryContext(context.Background(), q) // TODO: ComicSeriesId
+ 186 | if rows_err == nil {
+ 187 | defer rows.Close()
+ 188 | for rows.Next() {
+ 189 | var issue ComicIssue
+ 190 |
+ 191 | //var volume interface{}
+ 192 | var timelineDate interface{}
+ 193 | var series comicSeriesNullable
+ 194 | scan_err := rows.Scan(&issue.Id, &issue.Number, &issue.Name, &issue.Annual, &timelineDate, &issue.PublicationDate, &issue.Publisher, &issue.Date_added, &series.Id, &series.Name, &series.Miniseries, &series.StartYear, &series.Date_added)
+ 195 | if scan_err == nil {
+ 196 | /*if volume != nil {
+ 197 | tpb.Volume = int(volume.(int32))
+ 198 | }*/
+ 199 | if timelineDate != nil {
+ 200 | issue.TimelineDate = int(timelineDate.(int32))
+ 201 | }
+ 202 | issue.Series = comicSeriesScan(series)
+ 203 |
+ 204 | if lastUpdate.Before(issue.Date_added) {
+ 205 | lastUpdate = issue.Date_added
+ 206 | }
+ 207 | issues = append(issues, issue)
+ 208 | } else {
+ 209 | panic(scan_err)
+ 210 | }
+ 211 | }
+ 212 | } else {
+ 213 | panic(rows_err)
+ 214 | }
+ 215 |
+ 216 | return issues, lastUpdate
+ 217 | }
+ 218 |
+ 219 | func GetComicOneshots(conn *sql.DB, timeline bool) ([]ComicIssue, time.Time) {
+ 220 | lastUpdate := time.Time{}
+ 221 | var issues []ComicIssue
+ 222 | var q string
+ 223 | if timeline {
+ 224 | q = `SELECT r.ID, r."NUMBER", r."NAME", r.ANNUAL, r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED FROM COMICISSUES r WHERE r.COMICSERIESID IS NULL order by r.TIMELINEDATE ASC, r.NUMBER ASC`
+ 225 | } else {
+ 226 | q = `SELECT r.ID, r."NUMBER", r."NAME", r.ANNUAL, r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED FROM COMICISSUES r WHERE r.COMICSERIESID IS NULL order by r.PUBLICATIONDATE ASC, r.NUMBER ASC`
+ 227 | }
+ 228 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 229 | if rows_err == nil {
+ 230 | defer rows.Close()
+ 231 | for rows.Next() {
+ 232 | var issue ComicIssue
+ 233 | var timelineDate interface{}
+ 234 | scan_err := rows.Scan(&issue.Id, &issue.Number, &issue.Name, &issue.Annual, &timelineDate, &issue.PublicationDate, &issue.Publisher, &issue.Date_added)
+ 235 | if scan_err == nil {
+ 236 | /*if volume != nil {
+ 237 | tpb.Volume = int(volume.(int32))
+ 238 | }*/
+ 239 | if timelineDate != nil {
+ 240 | issue.TimelineDate = int(timelineDate.(int32))
+ 241 | }
+ 242 |
+ 243 | if lastUpdate.Before(issue.Date_added) {
+ 244 | lastUpdate = issue.Date_added
+ 245 | }
+ 246 | issues = append(issues, issue)
+ 247 | } else {
+ 248 | panic(scan_err)
+ 249 | }
+ 250 | }
+ 251 | } else {
+ 252 | panic(rows_err)
+ 253 | }
+ 254 |
+ 255 | return issues, lastUpdate
+ 256 | }
+ 257 |
+ 258 | func GetBookSeries(conn *sql.DB) ([]BookSeries, time.Time) {
+ 259 | lastUpdate := time.Time{}
+ 260 | var series []BookSeries
+ 261 | q := `SELECT r.ID, r."NAME", r.DATE_ADDED, (SELECT COUNT(*) FROM BOOKS WHERE BOOKS.BOOKSERIESID=r.ID) as bookcount FROM BOOKSERIES r`
+ 262 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 263 | if rows_err == nil {
+ 264 | defer rows.Close()
+ 265 | for rows.Next() {
+ 266 | var serie BookSeries
+ 267 | scan_err := rows.Scan(&serie.Id, &serie.Name, &serie.Date_added, &serie.BookCount)
+ 268 | if scan_err == nil {
+ 269 | if lastUpdate.Before(serie.Date_added) {
+ 270 | lastUpdate = serie.Date_added
+ 271 | }
+ 272 | series = append(series, serie)
+ 273 | }
+ 274 | }
+ 275 | }
+ 276 |
+ 277 | return series, lastUpdate
+ 278 | }
+ 279 |
+ 280 | func GetBooks(conn *sql.DB) ([]Book, time.Time) {
+ 281 | lastUpdate := time.Time{}
+ 282 | var books []Book
+ 283 | q := `SELECT r.ID, r."NUMBER", r."NAME", r.BOOKTYPE, r.AUTHOR, r.BOOKSERIESID, r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED, BOOKSERIES.ID, BOOKSERIES."NAME", BOOKSERIES.DATE_ADDED FROM BOOKS r LEFT JOIN BOOKSERIES ON BOOKSERIES.ID=r.BOOKSERIESID ORDER BY r.TIMELINEDATE ASC, r.NUMBER ASC`
+ 284 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 285 | if rows_err == nil {
+ 286 | defer rows.Close()
+ 287 | for rows.Next() {
+ 288 | var bookNull bookNullable
+ 289 | var bookSeriesNull bookSeriesNullable
+ 290 | scan_err := rows.Scan(&bookNull.Id, &bookNull.Number, &bookNull.Name, &bookNull.BookType, &bookNull.Author, &bookNull.BookSeriesId, &bookNull.TimelineDate, &bookNull.PublicationDate, &bookNull.Publisher, &bookNull.Date_added, &bookSeriesNull.Id, &bookSeriesNull.Name, &bookSeriesNull.Date_added)
+ 291 | if scan_err == nil {
+ 292 | book := bookScan(bookNull)
+ 293 | book.Series = bookSeriesScan(bookSeriesNull)
+ 294 | if lastUpdate.Before(book.Date_added) {
+ 295 | lastUpdate = book.Date_added
+ 296 | }
+ 297 | books = append(books, book)
+ 298 | }
+ 299 | }
+ 300 | }
+ 301 |
+ 302 | return books, lastUpdate
+ 303 | }
+ 304 |
+ 305 | func GetBookStandalones(conn *sql.DB) ([]Book, time.Time) {
+ 306 | lastUpdate := time.Time{}
+ 307 | var books []Book
+ 308 | q := `SELECT r.ID, r."NUMBER", r."NAME", r.BOOKTYPE, r.AUTHOR, r.BOOKSERIESID, r.TIMELINEDATE, r.PUBLICATIONDATE, r.PUBLISHER, r.DATE_ADDED FROM BOOKS r WHERE r.BOOKSERIESID IS NULL ORDER BY r.TIMELINEDATE ASC, r.NUMBER ASC`
+ 309 | rows, rows_err := conn.QueryContext(context.Background(), q)
+ 310 | if rows_err == nil {
+ 311 | defer rows.Close()
+ 312 | for rows.Next() {
+ 313 | var bookNull bookNullable
+ 314 | scan_err := rows.Scan(&bookNull.Id, &bookNull.Number, &bookNull.Name, &bookNull.BookType, &bookNull.Author, &bookNull.BookSeriesId, &bookNull.TimelineDate, &bookNull.PublicationDate, &bookNull.Publisher, &bookNull.Date_added)
+ 315 | if scan_err == nil {
+ 316 | book := bookScan(bookNull)
+ 317 | if lastUpdate.Before(book.Date_added) {
+ 318 | lastUpdate = book.Date_added
+ 319 | }
+ 320 | books = append(books, book)
+ 321 | }
+ 322 | }
+ 323 | }
+ 324 |
+ 325 | return books, lastUpdate
+ 326 | }
gemini/starwars/starwars.go (created)
+ 1 | package starwars
+ 2 |
+ 3 | import (
+ 4 | "database/sql"
+ 5 | "fmt"
+ 6 | "strings"
+ 7 | "time"
+ 8 |
+ 9 | "gitlab.com/clseibold/auragem_sis/db"
+ 10 | sis "gitlab.com/clseibold/smallnetinformationservices"
+ 11 | )
+ 12 |
+ 13 | var publishDate, _ = time.ParseInLocation(time.RFC3339, "2024-03-19T08:23:00", time.Local)
+ 14 |
+ 15 | func HandleStarWars(s sis.ServerHandle) {
+ 16 | conn := db.NewConn(db.StarWarsDB)
+ 17 | conn.SetMaxOpenConns(500)
+ 18 | conn.SetMaxIdleConns(3)
+ 19 | conn.SetConnMaxLifetime(time.Hour * 4)
+ 20 |
+ 21 | s.AddRoute("/starwars2", func(request sis.Request) {
+ 22 | request.Redirect("/starwars2/")
+ 23 | })
+ 24 |
+ 25 | s.AddRoute("/starwars2/", func(request sis.Request) {
+ 26 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T08:23:00", time.Local)
+ 27 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# Star Wars Database\n"})
+ 28 | if request.ScrollMetadataRequested {
+ 29 | request.SendAbstract("")
+ 30 | return
+ 31 | }
+ 32 |
+ 33 | request.Gemini(`# Star Wars Database
+ 34 |
+ 35 | Welcome to the Star Wars Database.
+ 36 |
+ 37 | ## Canon - Ordered By Timeline
+ 38 | => /starwars2/timeline/movies Movies
+ 39 | => /starwars2/timeline/shows TV Shows
+ 40 | => /starwars2/timeline/comics Comics
+ 41 | => /starwars2/timeline/bookseries Books
+ 42 | => /starwars2/timeline/all All
+ 43 |
+ 44 | ## Canon - Ordered By Publication
+ 45 | => /starwars2/publication/movies Movies
+ 46 | => /starwars2/publication/comics Comics
+ 47 |
+ 48 | => /starwars/ The Old Database
+ 49 | `)
+ 50 | })
+ 51 |
+ 52 | // Movies
+ 53 | s.AddRoute("/starwars2/timeline/movies", func(request sis.Request) {
+ 54 | handleMovies(request, conn, true)
+ 55 | })
+ 56 | s.AddRoute("/starwars2/publication/movies", func(request sis.Request) {
+ 57 | handleMovies(request, conn, false)
+ 58 | })
+ 59 |
+ 60 | // Movies CSV
+ 61 | s.AddRoute("/starwars2/timeline/movies/csv", func(request sis.Request) {
+ 62 | handleMoviesCSV(request, conn, true)
+ 63 | })
+ 64 | s.AddRoute("/starwars2/publication/movies/csv", func(request sis.Request) {
+ 65 | handleMoviesCSV(request, conn, false)
+ 66 | })
+ 67 |
+ 68 | s.AddRoute("/starwars2/timeline/shows", func(request sis.Request) {
+ 69 | shows, lastUpdate := GetShows(conn)
+ 70 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: TV Shows (Timeline)\n"})
+ 71 | if request.ScrollMetadataRequested {
+ 72 | request.SendAbstract("")
+ 73 | return
+ 74 | }
+ 75 |
+ 76 | header, tableData := constructTableDataFromShows(shows)
+ 77 | table := constructTable(header, tableData)
+ 78 |
+ 79 | var builder strings.Builder
+ 80 | fmt.Fprintf(&builder, "```\n%s\n```\n\n", table)
+ 81 |
+ 82 | request.Gemini(fmt.Sprintf(`# Star Wars Shows
+ 83 |
+ 84 | => /starwars2/ Home
+ 85 | => /starwars2/timeline/shows/episodes/ Episodes
+ 86 |
+ 87 | %s
+ 88 | `, builder.String()))
+ 89 | })
+ 90 |
+ 91 | s.AddRoute("/starwars2/timeline/comics", func(request sis.Request) {
+ 92 | fullSeries, lastUpdate := GetComicSeries_Full(conn)
+ 93 | miniseries, lastUpdate2 := GetComicSeries_Miniseries(conn)
+ 94 | if lastUpdate.Before(lastUpdate2) {
+ 95 | lastUpdate = lastUpdate2
+ 96 | }
+ 97 | crossovers, lastUpdate2 := GetComicCrossovers(conn, true)
+ 98 | if lastUpdate.Before(lastUpdate2) {
+ 99 | lastUpdate = lastUpdate2
+ 100 | }
+ 101 | oneshots, lastUpdate2 := GetComicOneshots(conn, true)
+ 102 | if lastUpdate.Before(lastUpdate2) {
+ 103 | lastUpdate = lastUpdate2
+ 104 | }
+ 105 |
+ 106 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comics (Timeline)\n"})
+ 107 | if request.ScrollMetadataRequested {
+ 108 | request.SendAbstract("")
+ 109 | return
+ 110 | }
+ 111 |
+ 112 | var builder strings.Builder
+ 113 | fmt.Fprintf(&builder, "## Full Series\n")
+ 114 | full_heading, full_data := constructTableDataFromSeries(fullSeries)
+ 115 | full_table := constructTable(full_heading, full_data)
+ 116 | fmt.Fprintf(&builder, "```\n%s```\n\n", full_table)
+ 117 |
+ 118 | fmt.Fprintf(&builder, "## Crossovers\n")
+ 119 | crossovers_heading, crossovers_data := constructTableDataFromCrossover(crossovers)
+ 120 | crossovers_table := constructTable(crossovers_heading, crossovers_data)
+ 121 | fmt.Fprintf(&builder, "```\n%s```\n\n", crossovers_table)
+ 122 |
+ 123 | fmt.Fprintf(&builder, "## Miniseries\n")
+ 124 | miniseries_heading, miniseries_data := constructTableDataFromSeries(miniseries)
+ 125 | miniseries_table := constructTable(miniseries_heading, miniseries_data)
+ 126 | fmt.Fprintf(&builder, "```\n%s```\n\n", miniseries_table)
+ 127 |
+ 128 | fmt.Fprintf(&builder, "## One-shots\n")
+ 129 | oneshots_heading, oneshots_data := constructTableDataFromOneshots(oneshots)
+ 130 | oneshots_table := constructTable(oneshots_heading, oneshots_data)
+ 131 | fmt.Fprintf(&builder, "```\n%s```\n\n", oneshots_table)
+ 132 |
+ 133 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - Series'
+ 134 |
+ 135 | => /starwars2/ Home
+ 136 | => /starwars2/timeline/comics/issues Issues
+ 137 | => /starwars2/timeline/comics/tpbs TPBs
+ 138 |
+ 139 | %s
+ 140 | `, builder.String()))
+ 141 | })
+ 142 |
+ 143 | s.AddRoute("/starwars2/publication/comics", func(request sis.Request) {
+ 144 | fullSeries, lastUpdate := GetComicSeries_Full(conn)
+ 145 | miniseries, lastUpdate2 := GetComicSeries_Miniseries(conn)
+ 146 | if lastUpdate.Before(lastUpdate2) {
+ 147 | lastUpdate = lastUpdate2
+ 148 | }
+ 149 | crossovers, lastUpdate2 := GetComicCrossovers(conn, false)
+ 150 | if lastUpdate.Before(lastUpdate2) {
+ 151 | lastUpdate = lastUpdate2
+ 152 | }
+ 153 | oneshots, lastUpdate2 := GetComicOneshots(conn, false)
+ 154 | if lastUpdate.Before(lastUpdate2) {
+ 155 | lastUpdate = lastUpdate2
+ 156 | }
+ 157 |
+ 158 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comics (Publication)\n"})
+ 159 | if request.ScrollMetadataRequested {
+ 160 | request.SendAbstract("")
+ 161 | return
+ 162 | }
+ 163 |
+ 164 | var builder strings.Builder
+ 165 | fmt.Fprintf(&builder, "## Full Series\n")
+ 166 | full_heading, full_data := constructTableDataFromSeries(fullSeries)
+ 167 | full_table := constructTable(full_heading, full_data)
+ 168 | fmt.Fprintf(&builder, "```\n%s```\n\n", full_table)
+ 169 |
+ 170 | fmt.Fprintf(&builder, "## Crossovers\n")
+ 171 | crossovers_heading, crossovers_data := constructTableDataFromCrossover(crossovers)
+ 172 | crossovers_table := constructTable(crossovers_heading, crossovers_data)
+ 173 | fmt.Fprintf(&builder, "```\n%s```\n\n", crossovers_table)
+ 174 |
+ 175 | fmt.Fprintf(&builder, "## Miniseries\n")
+ 176 | miniseries_heading, miniseries_data := constructTableDataFromSeries(miniseries)
+ 177 | miniseries_table := constructTable(miniseries_heading, miniseries_data)
+ 178 | fmt.Fprintf(&builder, "```\n%s```\n\n", miniseries_table)
+ 179 |
+ 180 | fmt.Fprintf(&builder, "## One-shots\n")
+ 181 | oneshots_heading, oneshots_data := constructTableDataFromOneshots(oneshots)
+ 182 | oneshots_table := constructTable(oneshots_heading, oneshots_data)
+ 183 | fmt.Fprintf(&builder, "```\n%s```\n\n", oneshots_table)
+ 184 |
+ 185 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - Series'
+ 186 |
+ 187 | => /starwars2/ Home
+ 188 | => /starwars2/publication/comics/issues Issues
+ 189 | => /starwars2/publication/comics/tpbs TPBs
+ 190 |
+ 191 | %s
+ 192 | `, builder.String()))
+ 193 | })
+ 194 |
+ 195 | s.AddRoute("/starwars2/timeline/comics/tpbs", func(request sis.Request) {
+ 196 | tpbs, lastUpdate := GetTPBs(conn, true)
+ 197 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comic TPBs (Timeline)\n"})
+ 198 | if request.ScrollMetadataRequested {
+ 199 | request.SendAbstract("")
+ 200 | return
+ 201 | }
+ 202 |
+ 203 | heading, data := constructTableDataFromTPBs(tpbs)
+ 204 | table := constructTable(heading, data)
+ 205 |
+ 206 | var builder strings.Builder
+ 207 | fmt.Fprintf(&builder, "```\n%s```\n\n", table)
+ 208 |
+ 209 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - TPBs
+ 210 |
+ 211 | => /starwars2/ Home
+ 212 | => /starwars2/timeline/comics Comic Series'
+ 213 | => /starwars2/timeline/comics/issues Issues
+ 214 | => /starwars2/timeline/comics/tpbs TPBs
+ 215 |
+ 216 | %s
+ 217 | `, builder.String()))
+ 218 | })
+ 219 |
+ 220 | s.AddRoute("/starwars2/publication/comics/tpbs", func(request sis.Request) {
+ 221 | tpbs, lastUpdate := GetTPBs(conn, false)
+ 222 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comic TPBs (Publication)\n"})
+ 223 | if request.ScrollMetadataRequested {
+ 224 | request.SendAbstract("")
+ 225 | return
+ 226 | }
+ 227 |
+ 228 | heading, data := constructTableDataFromTPBs(tpbs)
+ 229 | table := constructTable(heading, data)
+ 230 |
+ 231 | var builder strings.Builder
+ 232 | fmt.Fprintf(&builder, "```\n%s```\n\n", table)
+ 233 |
+ 234 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - TPBs
+ 235 |
+ 236 | => /starwars2/ Home
+ 237 | => /starwars2/publication/comics Comic Series'
+ 238 | => /starwars2/publication/comics/issues Issues
+ 239 | => /starwars2/publication/comics/tpbs TPBs
+ 240 |
+ 241 | %s
+ 242 | `, builder.String()))
+ 243 | })
+ 244 |
+ 245 | s.AddRoute("/starwars2/timeline/comics/issues", func(request sis.Request) {
+ 246 | issues, lastUpdate := GetComicIssues(conn, true)
+ 247 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comic Issues (Timeline)\n"})
+ 248 | if request.ScrollMetadataRequested {
+ 249 | request.SendAbstract("")
+ 250 | return
+ 251 | }
+ 252 |
+ 253 | heading, data := constructTableDataFromIssues(issues)
+ 254 | table := constructTable(heading, data)
+ 255 |
+ 256 | var builder strings.Builder
+ 257 | fmt.Fprintf(&builder, "```\n%s```\n\n", table)
+ 258 |
+ 259 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - Issues
+ 260 |
+ 261 | => /starwars2/ Home
+ 262 | => /starwars2/timeline/comics Comic Series'
+ 263 | => /starwars2/timeline/comics/issues Issues
+ 264 | => /starwars2/timeline/comics/tpbs TPBs
+ 265 |
+ 266 | %s
+ 267 | `, builder.String()))
+ 268 | })
+ 269 |
+ 270 | s.AddRoute("/starwars2/publication/comics/issues", func(request sis.Request) {
+ 271 | issues, lastUpdate := GetComicIssues(conn, false)
+ 272 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Comic Issues (Publication)\n"})
+ 273 | if request.ScrollMetadataRequested {
+ 274 | request.SendAbstract("")
+ 275 | return
+ 276 | }
+ 277 |
+ 278 | heading, data := constructTableDataFromIssues(issues)
+ 279 | table := constructTable(heading, data)
+ 280 |
+ 281 | var builder strings.Builder
+ 282 | fmt.Fprintf(&builder, "```\n%s```\n\n", table)
+ 283 |
+ 284 | request.Gemini(fmt.Sprintf(`# Star Wars Comics - Issues
+ 285 |
+ 286 | => /starwars2/ Home
+ 287 | => /starwars2/publication/comics Comic Series'
+ 288 | => /starwars2/publication/comics/issues Issues
+ 289 | => /starwars2/publication/comics/tpbs TPBs
+ 290 |
+ 291 | %s
+ 292 | `, builder.String()))
+ 293 | })
+ 294 |
+ 295 | s.AddRoute("/starwars2/timeline/bookseries", func(request sis.Request) {
+ 296 | var builder strings.Builder
+ 297 |
+ 298 | series, lastUpdate := GetBookSeries(conn)
+ 299 | series_header, series_tableData := constructTableDataFromBookSeries(series)
+ 300 | series_table := constructTable(series_header, series_tableData)
+ 301 | fmt.Fprintf(&builder, "## Series'\n```\n%s\n```\n\n", series_table)
+ 302 |
+ 303 | standalones, lastUpdate2 := GetBookStandalones(conn)
+ 304 | if lastUpdate.Before(lastUpdate2) {
+ 305 | lastUpdate = lastUpdate2
+ 306 | }
+ 307 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Book Series (Timeline)\n"})
+ 308 | if request.ScrollMetadataRequested {
+ 309 | request.SendAbstract("")
+ 310 | return
+ 311 | }
+ 312 |
+ 313 | standalones_header, standalones_tableData := constructTableDataFromBookStandalones(standalones)
+ 314 | standalones_table := constructTable(standalones_header, standalones_tableData)
+ 315 | fmt.Fprintf(&builder, "## Standalones\n```\n%s\n```\n\n", standalones_table)
+ 316 |
+ 317 | request.Gemini(fmt.Sprintf(`# Star Wars Book Series'
+ 318 |
+ 319 | => /starwars2/ Home
+ 320 | => /starwars2/timeline/bookseries Book Series'
+ 321 | => /starwars2/timeline/books Books
+ 322 |
+ 323 | %s
+ 324 | `, builder.String()))
+ 325 | })
+ 326 |
+ 327 | s.AddRoute("/starwars2/timeline/books", func(request sis.Request) {
+ 328 | books, lastUpdate := GetBooks(conn)
+ 329 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Books (Timeline)\n"})
+ 330 | if request.ScrollMetadataRequested {
+ 331 | request.SendAbstract("")
+ 332 | return
+ 333 | }
+ 334 |
+ 335 | header, tableData := constructTableDataFromBooks(books)
+ 336 | table := constructTable(header, tableData)
+ 337 |
+ 338 | var builder strings.Builder
+ 339 | fmt.Fprintf(&builder, "```\n%s\n```\n\n", table)
+ 340 |
+ 341 | request.Gemini(fmt.Sprintf(`# Star Wars Books
+ 342 |
+ 343 | => /starwars2/ Home
+ 344 | => /starwars2/timeline/bookseries Book Series'
+ 345 | => /starwars2/timeline/books Books
+ 346 |
+ 347 | %s
+ 348 | `, builder.String()))
+ 349 | })
+ 350 | }
+ 351 |
+ 352 | func handleMovies(request sis.Request, conn *sql.DB, timeline bool) {
+ 353 | movies, lastUpdate := GetMovies(conn, timeline)
+ 354 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Movies\n"})
+ 355 | if request.ScrollMetadataRequested {
+ 356 | request.SendAbstract("")
+ 357 | return
+ 358 | }
+ 359 |
+ 360 | header, tableData := constructTableDataFromMovies(movies)
+ 361 | table := constructTable(header, tableData)
+ 362 |
+ 363 | var builder strings.Builder
+ 364 | fmt.Fprintf(&builder, "```\n%s\n```\n", table)
+ 365 |
+ 366 | request.Gemini(fmt.Sprintf(`# Star Wars Movies
+ 367 |
+ 368 | => /starwars2/ Home
+ 369 |
+ 370 | %s
+ 371 | => movies/csv CSV File
+ 372 | `, builder.String()))
+ 373 | }
+ 374 |
+ 375 | func handleMoviesCSV(request sis.Request, conn *sql.DB, timeline bool) {
+ 376 | movies, lastUpdate := GetMovies(conn, timeline)
+ 377 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: publishDate, UpdateDate: lastUpdate, Language: "en", Abstract: "# Star Wars Database: Movies CSV\n"})
+ 378 | if request.ScrollMetadataRequested {
+ 379 | request.SendAbstract("text/csv")
+ 380 | return
+ 381 | }
+ 382 |
+ 383 | header, tableData := constructTableDataFromMovies(movies)
+ 384 |
+ 385 | var builder strings.Builder
+ 386 | for colNum, col := range header {
+ 387 | fmt.Fprintf(&builder, "%s", col)
+ 388 | if colNum < len(header)-1 {
+ 389 | fmt.Fprintf(&builder, ",")
+ 390 | }
+ 391 | }
+ 392 | fmt.Fprintf(&builder, "\n")
+ 393 |
+ 394 | for _, row := range tableData {
+ 395 | for colNum, col := range row {
+ 396 | fmt.Fprintf(&builder, "%s", col)
+ 397 | if colNum < len(row)-1 {
+ 398 | fmt.Fprintf(&builder, ",")
+ 399 | }
+ 400 | }
+ 401 | fmt.Fprintf(&builder, "\n")
+ 402 | }
+ 403 |
+ 404 | request.TextWithMimetype("text/csv", builder.String())
+ 405 | //return c.Blob("text/csv", []byte(builder.String()))
+ 406 | }
gemini/starwars/tables.go (created)
+ 1 | package starwars
+ 2 |
+ 3 | import (
+ 4 | "strings"
+ 5 | "math"
+ 6 | "fmt"
+ 7 | "strconv"
+ 8 | "github.com/rivo/uniseg"
+ 9 | )
+ 10 |
+ 11 | func constructTableDataFromMovies(movies []Movie) ([]string, [][]string) {
+ 12 | var rows [][]string
+ 13 | for _, movie := range movies {
+ 14 | cols := make([]string, 3)
+ 15 | cols[0] = movie.Name
+ 16 | year, aby := convertIntToStarWarsYear(movie.TimelineDate)
+ 17 | if !aby {
+ 18 | cols[1] = fmt.Sprintf("%d BBY", year)
+ 19 | } else {
+ 20 | cols[1] = fmt.Sprintf("%d ABY", year)
+ 21 | }
+ 22 | year, month, day := movie.PublicationDate.Date()
+ 23 | cols[2] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 24 |
+ 25 | rows = append(rows, cols)
+ 26 | }
+ 27 | return []string { "TITLE", "BBY/ABY", "PUBLICATION" }, rows
+ 28 | }
+ 29 |
+ 30 | func constructTableDataFromShows(shows []TVShow) ([]string, [][]string) {
+ 31 | var rows [][]string
+ 32 | for _, show := range shows {
+ 33 | cols := make([]string, 1)
+ 34 | cols[0] = show.Name
+ 35 |
+ 36 | rows = append(rows, cols)
+ 37 | }
+ 38 | return []string { "TITLE" }, rows
+ 39 | }
+ 40 |
+ 41 | func constructTableDataFromSeries(series []ComicSeries) ([]string, [][]string) {
+ 42 | var rows [][]string
+ 43 | for _, serie := range series {
+ 44 | cols := make([]string, 4)
+ 45 | cols[0] = serie.Name
+ 46 | cols[1] = strconv.Itoa(serie.IssueCount)
+ 47 | cols[2] = strconv.Itoa(serie.TPBCount)
+ 48 | cols[3] = strconv.Itoa(serie.StartYear)
+ 49 |
+ 50 | rows = append(rows, cols)
+ 51 | }
+ 52 | return []string { "TITLE", "ISSUES", "TPBs", "YEAR" }, rows
+ 53 | }
+ 54 |
+ 55 | func constructTableDataFromCrossover(tpbs []ComicTPB) ([]string, [][]string) {
+ 56 | var rows [][]string
+ 57 | for _, tpb := range tpbs {
+ 58 | cols := make([]string, 2)
+ 59 | cols[0] = tpb.Name
+ 60 | year, month, day := tpb.PublicationDate.Date()
+ 61 | cols[1] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 62 |
+ 63 | rows = append(rows, cols)
+ 64 | }
+ 65 | return []string { "TITLE", "PUBLICATION" }, rows
+ 66 | }
+ 67 |
+ 68 | func constructTableDataFromTPBs(tpbs []ComicTPB) ([]string, [][]string) {
+ 69 | var rows [][]string
+ 70 | for _, tpb := range tpbs {
+ 71 | cols := make([]string, 5)
+ 72 | if tpb.Series.Name != "" {
+ 73 | cols[0] = fmt.Sprintf("%s (%d)", tpb.Series.Name, tpb.Series.StartYear)
+ 74 | } else {
+ 75 | cols[0] = "-"
+ 76 | }
+ 77 | cols[1] = strconv.Itoa(tpb.Volume)
+ 78 | cols[2] = tpb.Name
+ 79 | cols[3] = strconv.Itoa(tpb.IssueCount)
+ 80 | year, month, day := tpb.PublicationDate.Date()
+ 81 | cols[4] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 82 |
+ 83 | rows = append(rows, cols)
+ 84 | }
+ 85 | return []string { "SERIES", "VOL", "TITLE", "ISSUES", "PUBLICATION" }, rows
+ 86 | }
+ 87 |
+ 88 | func constructTableDataFromIssues(issues []ComicIssue) ([]string, [][]string) {
+ 89 | var rows [][]string
+ 90 | for _, issue := range issues {
+ 91 | cols := make([]string, 6)
+ 92 | if issue.Series.Name != "" {
+ 93 | cols[0] = fmt.Sprintf("%s (%d)", issue.Series.Name, issue.Series.StartYear)
+ 94 | cols[1] = strconv.Itoa(issue.Number)
+ 95 | if issue.Annual {
+ 96 | cols[1] = "A" + cols[1]
+ 97 | }
+ 98 | } else {
+ 99 | cols[0] = "-"
+ 100 | cols[1] = "-"
+ 101 | }
+ 102 | cols[2] = issue.Name
+ 103 |
+ 104 | year, aby := convertIntToStarWarsYear(issue.TimelineDate)
+ 105 | if !aby {
+ 106 | cols[3] = fmt.Sprintf("%d BBY", year)
+ 107 | } else {
+ 108 | cols[3] = fmt.Sprintf("%d ABY", year)
+ 109 | }
+ 110 |
+ 111 | year, month, day := issue.PublicationDate.Date()
+ 112 | cols[4] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 113 | cols[5] = issue.Publisher
+ 114 |
+ 115 | rows = append(rows, cols)
+ 116 | }
+ 117 | return []string { "SERIES", "#", "TITLE", "BBY/ABY", "PUBLICATION", "PUBLISHER" }, rows
+ 118 | }
+ 119 |
+ 120 | func constructTableDataFromOneshots(issues []ComicIssue) ([]string, [][]string) {
+ 121 | var rows [][]string
+ 122 | for _, issue := range issues {
+ 123 | cols := make([]string, 4)
+ 124 | cols[0] = issue.Name
+ 125 |
+ 126 | year, aby := convertIntToStarWarsYear(issue.TimelineDate)
+ 127 | if !aby {
+ 128 | cols[1] = fmt.Sprintf("%d BBY", year)
+ 129 | } else {
+ 130 | cols[1] = fmt.Sprintf("%d ABY", year)
+ 131 | }
+ 132 |
+ 133 | year, month, day := issue.PublicationDate.Date()
+ 134 | cols[2] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 135 | cols[3] = issue.Publisher
+ 136 |
+ 137 | rows = append(rows, cols)
+ 138 | }
+ 139 | return []string { "TITLE", "BBY/ABY", "PUBLICATION", "PUBLISHER" }, rows
+ 140 | }
+ 141 |
+ 142 | func constructTableDataFromBookSeries(series []BookSeries) ([]string, [][]string) {
+ 143 | var rows [][]string
+ 144 | for _, serie := range series {
+ 145 | cols := make([]string, 2)
+ 146 | cols[0] = serie.Name
+ 147 | cols[1] = strconv.Itoa(serie.BookCount)
+ 148 |
+ 149 | rows = append(rows, cols)
+ 150 | }
+ 151 | return []string { "TITLE", "BOOKS" }, rows
+ 152 | }
+ 153 |
+ 154 | func constructTableDataFromBooks(books []Book) ([]string, [][]string) {
+ 155 | var rows [][]string
+ 156 | for _, book := range books {
+ 157 | cols := make([]string, 6)
+ 158 | if book.Series.Name != "" {
+ 159 | cols[0] = fmt.Sprintf("%s", book.Series.Name)
+ 160 | cols[1] = strconv.Itoa(book.Number)
+ 161 | } else {
+ 162 | cols[0] = "-"
+ 163 | cols[1] = "-"
+ 164 | }
+ 165 |
+ 166 | cols[2] = book.Name
+ 167 | cols[3] = book.Author
+ 168 |
+ 169 | year, aby := convertIntToStarWarsYear(book.TimelineDate)
+ 170 | if !aby {
+ 171 | cols[4] = fmt.Sprintf("%d BBY", year)
+ 172 | } else {
+ 173 | cols[4] = fmt.Sprintf("%d ABY", year)
+ 174 | }
+ 175 |
+ 176 | year, month, day := book.PublicationDate.Date()
+ 177 | cols[5] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 178 |
+ 179 | rows = append(rows, cols)
+ 180 | }
+ 181 | return []string { "SERIES", "#", "TITLE", "AUTHOR", "BBY/ABY", "PUBLICATION" }, rows // TODO: Add Publisher?
+ 182 | }
+ 183 |
+ 184 | func constructTableDataFromBookStandalones(books []Book) ([]string, [][]string) {
+ 185 | var rows [][]string
+ 186 | for _, book := range books {
+ 187 | cols := make([]string, 4)
+ 188 |
+ 189 | cols[0] = book.Name
+ 190 | cols[1] = book.Author
+ 191 |
+ 192 | year, aby := convertIntToStarWarsYear(book.TimelineDate)
+ 193 | if !aby {
+ 194 | cols[2] = fmt.Sprintf("%d BBY", year)
+ 195 | } else {
+ 196 | cols[2] = fmt.Sprintf("%d ABY", year)
+ 197 | }
+ 198 |
+ 199 | year, month, day := book.PublicationDate.Date()
+ 200 | cols[3] = fmt.Sprintf("%04d-%02d-%02d", year, month, day)
+ 201 |
+ 202 | rows = append(rows, cols)
+ 203 | }
+ 204 | return []string { "TITLE", "AUTHOR", "BBY/ABY", "PUBLICATION" }, rows // TODO: Add Publisher?
+ 205 | }
+ 206 |
+ 207 | func constructTable(headingRow []string, data [][]string) string {
+ 208 | if len(data) == 0 {
+ 209 | return ""
+ 210 | }
+ 211 | cellLengthLimit := 37
+ 212 | var builder strings.Builder
+ 213 |
+ 214 | // Get maximum length of each column and number of lines (for overflow)
+ 215 | colLengths := make([]int, len(data[0]))
+ 216 | rowLines := make([]int, len(data))
+ 217 | for colNum, col := range headingRow {
+ 218 | graphemeCount := uniseg.GraphemeClusterCount(col)
+ 219 | if graphemeCount > colLengths[colNum] && graphemeCount <= cellLengthLimit {
+ 220 | colLengths[colNum] = graphemeCount
+ 221 | }
+ 222 | }
+ 223 | for rowNum, row := range data {
+ 224 | for colNum, col := range row {
+ 225 | graphemeCount := uniseg.GraphemeClusterCount(col)
+ 226 | if graphemeCount > colLengths[colNum] && graphemeCount <= cellLengthLimit {
+ 227 | colLengths[colNum] = graphemeCount
+ 228 | }
+ 229 |
+ 230 | lines := int(math.Ceil(float64(graphemeCount) / float64(cellLengthLimit)))
+ 231 | if lines > rowLines[rowNum] {
+ 232 | rowLines[rowNum] = lines
+ 233 | }
+ 234 | }
+ 235 | }
+ 236 |
+ 237 | // Construct heading row - First Line
+ 238 | for colNum, _ := range headingRow {
+ 239 | length := colLengths[colNum]
+ 240 |
+ 241 | if colNum == 0 {
+ 242 | fmt.Fprintf(&builder, "╔═")
+ 243 | } else {
+ 244 | fmt.Fprintf(&builder, "╤═")
+ 245 | }
+ 246 |
+ 247 | for i := 0; i < length + 1; i++ {
+ 248 | fmt.Fprintf(&builder, "═")
+ 249 | }
+ 250 | }
+ 251 | fmt.Fprintf(&builder, "╗\n")
+ 252 |
+ 253 | // Heading Row Contents
+ 254 | for colNum, col := range headingRow {
+ 255 | diff := colLengths[colNum] - len(col)
+ 256 |
+ 257 | if colNum == 0 {
+ 258 | fmt.Fprintf(&builder, "║ ")
+ 259 | } else {
+ 260 | fmt.Fprintf(&builder, "│ ")
+ 261 | }
+ 262 |
+ 263 | fmt.Fprintf(&builder, "%s", col)
+ 264 | for i := 0; i < diff + 1; i++ {
+ 265 | fmt.Fprintf(&builder, " ")
+ 266 | }
+ 267 | }
+ 268 | fmt.Fprintf(&builder, "║\n")
+ 269 |
+ 270 | // Heading Row Bottom
+ 271 | for colNum, _ := range headingRow {
+ 272 | length := colLengths[colNum]
+ 273 |
+ 274 | if colNum == 0 {
+ 275 | fmt.Fprintf(&builder, "╠═")
+ 276 | } else {
+ 277 | fmt.Fprintf(&builder, "╪═")
+ 278 | }
+ 279 |
+ 280 | for i := 0; i < length + 1; i++ {
+ 281 | fmt.Fprintf(&builder, "═")
+ 282 | }
+ 283 | }
+ 284 | fmt.Fprintf(&builder, "╣\n")
+ 285 |
+ 286 | // Data
+ 287 | for rowNum, row := range data { // TODO: I'm pretty sure this is very slow
+ 288 | // Contents
+ 289 | for rowLine := 1; rowLine <= rowLines[rowNum]; rowLine++ {
+ 290 | for colNum, col := range row {
+ 291 | if colNum == 0 {
+ 292 | fmt.Fprintf(&builder, "║ ")
+ 293 | } else {
+ 294 | fmt.Fprintf(&builder, "│ ")
+ 295 | }
+ 296 |
+ 297 | graphemeCount := 0
+ 298 | graphemeIndex := 0
+ 299 | gr := uniseg.NewGraphemes(col)
+ 300 |
+ 301 | start := (cellLengthLimit * rowLine) - cellLengthLimit - 1
+ 302 | end := (cellLengthLimit * rowLine) - 1
+ 303 | for gr.Next() {
+ 304 | if graphemeIndex >= start && graphemeIndex < end {
+ 305 | graphemeCount++
+ 306 | runes := gr.Runes()
+ 307 | fmt.Fprintf(&builder, "%s", string(runes))
+ 308 | }
+ 309 | graphemeIndex++
+ 310 | }
+ 311 |
+ 312 | diff := colLengths[colNum] - graphemeCount
+ 313 |
+ 314 | /*if len(runes) > 0 {
+ 315 | start := (cellLengthLimit * rowLine) - cellLengthLimit
+ 316 | if start < len(runes) {
+ 317 | end := (cellLengthLimit * rowLine)
+ 318 | if end >= len(runes) {
+ 319 | end = len(runes)
+ 320 | }
+ 321 | content := string(runes[start:end])
+ 322 | fmt.Fprintf(&builder, "%s", content)
+ 323 | diff = colLengths[colNum] - len(content)
+ 324 | }
+ 325 | }*/
+ 326 | for i := 0; i < diff + 1; i++ {
+ 327 | fmt.Fprintf(&builder, " ")
+ 328 | }
+ 329 | }
+ 330 | fmt.Fprintf(&builder, "║\n")
+ 331 | }
+ 332 |
+ 333 | // Bottom
+ 334 | if rowNum == len(data) - 1 {
+ 335 | for colNum, _ := range row {
+ 336 | length := colLengths[colNum]
+ 337 |
+ 338 | if colNum == 0 {
+ 339 | fmt.Fprintf(&builder, "╚═")
+ 340 | } else {
+ 341 | fmt.Fprintf(&builder, "╧═")
+ 342 | }
+ 343 |
+ 344 | for i := 0; i < length + 1; i++ {
+ 345 | fmt.Fprintf(&builder, "═")
+ 346 | }
+ 347 | }
+ 348 | fmt.Fprintf(&builder, "╝\n")
+ 349 | } else {
+ 350 | for colNum, _ := range row {
+ 351 | length := colLengths[colNum]
+ 352 |
+ 353 | if colNum == 0 {
+ 354 | fmt.Fprintf(&builder, "╟─")
+ 355 | } else {
+ 356 | fmt.Fprintf(&builder, "┼─")
+ 357 | }
+ 358 |
+ 359 | for i := 0; i < length + 1; i++ {
+ 360 | fmt.Fprintf(&builder, "─")
+ 361 | }
+ 362 | }
+ 363 | fmt.Fprintf(&builder, "╢\n")
+ 364 | }
+ 365 | }
+ 366 |
+ 367 | return builder.String()
+ 368 | }
gemini/starwars/types.go (created)
+ 1 | package starwars
+ 2 |
+ 3 | import (
+ 4 | "time"
+ 5 | )
+ 6 |
+ 7 | // Franchise
+ 8 |
+ 9 | type ComicSeries struct {
+ 10 | Id int
+ 11 | Name string
+ 12 | Oneoff bool
+ 13 | Miniseries bool
+ 14 | StartYear int
+ 15 | Date_added time.Time
+ 16 |
+ 17 | TPBCount int
+ 18 | IssueCount int
+ 19 | // AnnualCount int
+ 20 | }
+ 21 |
+ 22 | type comicSeriesNullable struct {
+ 23 | Id interface{}
+ 24 | Name interface{}
+ 25 | Oneoff interface{}
+ 26 | Miniseries interface{}
+ 27 | StartYear interface{}
+ 28 | Date_added interface{}
+ 29 | }
+ 30 |
+ 31 | func comicSeriesScan(series comicSeriesNullable) ComicSeries {
+ 32 | var result ComicSeries
+ 33 | if series.Id != nil {
+ 34 | result.Id = int(series.Id.(int32))
+ 35 | }
+ 36 | if series.Name != nil {
+ 37 | result.Name = string(series.Name.([]uint8))
+ 38 | }
+ 39 | if series.Oneoff != nil {
+ 40 | result.Oneoff = series.Oneoff.(bool)
+ 41 | }
+ 42 | if series.Miniseries != nil {
+ 43 | result.Miniseries = series.Miniseries.(bool)
+ 44 | }
+ 45 | if series.StartYear != nil {
+ 46 | result.StartYear = int(series.StartYear.(int32))
+ 47 | }
+ 48 | if series.Date_added != nil {
+ 49 | result.Date_added = series.Date_added.(time.Time)
+ 50 | }
+ 51 |
+ 52 | return result
+ 53 | }
+ 54 |
+ 55 | type ComicTPB struct {
+ 56 | Id int
+ 57 | Volume int
+ 58 | Name string
+ 59 | Crossover bool
+ 60 |
+ 61 | ComicSeriesId int // Optional
+ 62 |
+ 63 | TimelineDate int // NOTE: Only used if TPB doesn't consist of individual issues
+ 64 | PublicationDate time.Time
+ 65 | Date_added time.Time
+ 66 |
+ 67 | Series ComicSeries
+ 68 | IssueCount int
+ 69 | }
+ 70 |
+ 71 | type ComicIssue struct {
+ 72 | Id int
+ 73 | Number int
+ 74 | Name string
+ 75 | Annual bool
+ 76 |
+ 77 | ComicSeriesId int
+ 78 | ComicTPBId int
+ 79 | ComicCrossoverId int
+ 80 |
+ 81 | TimelineDate int
+ 82 | PublicationDate time.Time
+ 83 | Publisher string // Marvel, IDW
+ 84 | Date_added time.Time
+ 85 |
+ 86 | Series ComicSeries
+ 87 | //TPB ComicTPB // TODO
+ 88 | }
+ 89 |
+ 90 | /*type ComicOmnibus struct {
+ 91 | Id int
+ 92 | Volume int
+ 93 | Name string
+ 94 | ComicSeriesId int
+ 95 | TimelineDate string
+ 96 | PublicationDate time.Time
+ 97 | Date_added int
+ 98 | }*/
+ 99 |
+ 100 | type BookSeries struct {
+ 101 | Id int
+ 102 | Name string
+ 103 | Date_added time.Time
+ 104 |
+ 105 | BookCount int
+ 106 | }
+ 107 |
+ 108 | type bookSeriesNullable struct {
+ 109 | Id interface{}
+ 110 | Name interface{}
+ 111 | Date_added interface{}
+ 112 | }
+ 113 |
+ 114 |
+ 115 | func bookSeriesScan(serie bookSeriesNullable) BookSeries {
+ 116 | var result BookSeries
+ 117 | if serie.Id != nil {
+ 118 | result.Id = int(serie.Id.(int32))
+ 119 | }
+ 120 | if serie.Name != nil {
+ 121 | result.Name = string(serie.Name.([]uint8))
+ 122 | }
+ 123 | if serie.Date_added != nil {
+ 124 | result.Date_added = serie.Date_added.(time.Time)
+ 125 | }
+ 126 |
+ 127 | return result
+ 128 | }
+ 129 |
+ 130 | type Book struct {
+ 131 | Id int
+ 132 | Number int
+ 133 | Name string
+ 134 | BookType string // Adult, YA, Junior, Young, ShortStory
+ 135 | Author string
+ 136 | BookSeriesId int
+ 137 | TimelineDate int
+ 138 | PublicationDate time.Time
+ 139 | Publisher string
+ 140 | Date_added time.Time
+ 141 |
+ 142 | Series BookSeries
+ 143 | }
+ 144 |
+ 145 | type bookNullable struct {
+ 146 | Id interface{}
+ 147 | Number interface{}
+ 148 | Name interface{}
+ 149 | BookType interface{}
+ 150 | Author interface{}
+ 151 | BookSeriesId interface{}
+ 152 | TimelineDate interface{}
+ 153 | PublicationDate interface{}
+ 154 | Publisher interface{}
+ 155 | Date_added interface{}
+ 156 | }
+ 157 |
+ 158 | func bookScan(book bookNullable) Book {
+ 159 | var result Book
+ 160 | if book.Id != nil {
+ 161 | result.Id = int(book.Id.(int32))
+ 162 | }
+ 163 | if book.Number != nil {
+ 164 | result.Number = int(book.Number.(int32))
+ 165 | }
+ 166 | if book.Name != nil {
+ 167 | result.Name = string(book.Name.([]uint8))
+ 168 | }
+ 169 | if book.BookType != nil {
+ 170 | result.BookType = string(book.BookType.([]uint8))
+ 171 | }
+ 172 | if book.Author != nil {
+ 173 | result.Author = string(book.Author.([]uint8))
+ 174 | }
+ 175 | if book.BookSeriesId != nil {
+ 176 | result.BookSeriesId = int(book.BookSeriesId.(int32))
+ 177 | }
+ 178 | if book.TimelineDate != nil {
+ 179 | result.TimelineDate = int(book.TimelineDate.(int32))
+ 180 | }
+ 181 | if book.PublicationDate != nil {
+ 182 | result.PublicationDate = book.PublicationDate.(time.Time)
+ 183 | }
+ 184 | if book.Publisher != nil {
+ 185 | result.Publisher = string(book.Publisher.([]uint8))
+ 186 | }
+ 187 | if book.Date_added != nil {
+ 188 | result.Date_added = book.Date_added.(time.Time)
+ 189 | }
+ 190 |
+ 191 | return result
+ 192 | }
+ 193 |
+ 194 | type Movie struct {
+ 195 | Id int
+ 196 | Name string
+ 197 | TimelineDate int
+ 198 | PublicationDate time.Time
+ 199 | ProductionCompany string
+ 200 | Distributor string
+ 201 | Date_added time.Time
+ 202 | }
+ 203 |
+ 204 | type TVShow struct {
+ 205 | Id int
+ 206 | Name string
+ 207 | Date_added time.Time
+ 208 | }
+ 209 |
+ 210 | type TVShowEpisode struct {
+ 211 | Id int
+ 212 | Number int
+ 213 | Season int
+ 214 | Name string
+ 215 | TvShowId int
+ 216 | TimelineDate int
+ 217 | PublicationDate time.Time
+ 218 | Date_added time.Time
+ 219 | }
+ 220 |
+ 221 | // -1 = 0 BBY; 0 = 0 ABY
+ 222 | // (-timeline + 1) * -1 = BBY; BBY - 1 = -timeline
+ 223 | func convertStarWarsYearToInt(year int, aby bool) int {
+ 224 | if !aby {
+ 225 | return year - 1
+ 226 | } else {
+ 227 | return year
+ 228 | }
+ 229 | }
+ 230 |
+ 231 | func convertIntToStarWarsYear(timeline int) (int, bool) {
+ 232 | if timeline < 0 { // BBY
+ 233 | return (timeline + 1) * -1, false
+ 234 | } else { // ABY
+ 235 | return timeline, true
+ 236 | }
+ 237 | }
gemini/textola/textola.go
... | ...
170 | filedata, _ := s.GetServer().FS.ReadFile("the_cask_of_amontillado.txt")
171 | return makeTextFromString("Presenting Edgar Allan Poe's The Cask of Amontillado", string(filedata), TextolaContentType_OldClassicsFiction)
172 | }
173 |
174 | func HandleTextola(s sis.ServerHandle) {
- 175 | //fmt.Printf("GuestbookText: %s\n", theCaskOfAmontillado)
+ 175 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
+ 176 | updateDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
176 | var context *TextolaContext = &TextolaContext{
177 | currentText: getTextFromSchedule(s),
178 | mutex: sync.RWMutex{},
179 | }
180 | context.readCond = sync.NewCond(context.mutex.RLocker())
... | ...
181 |
182 | //fmt.Printf("Textola Text: (Lines %d) %#v\n", len(context.currentText.lines), context.currentText.lines)
183 |
184 | var connectedClients atomic.Int64
185 | s.AddRoute("/textola/", func(request sis.Request) {
+ 187 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: updateDate, Language: "en", Abstract: "# Textola\n"})
+ 188 | if request.ScrollMetadataRequested {
+ 189 | request.SendAbstract("")
+ 190 | return
+ 191 | }
186 | request.Gemini("# Textola\n\n")
187 | limiter := rate.NewLimiter(rate.Every(time.Second), 1)
188 |
189 | connectedClients.Add(1)
190 |
gemini/texts/christianity/christianity.go
... | ...
92 |
93 | // Cache the books from the ASV version of the bible. These should be the same for the ESV bible as well. Note: This does not include the apocrypha.
94 | asvBooks := GetBooks(englishBibleVersions[0].Id, apiKey)
95 |
96 | g.AddRoute("/scriptures/christian/", func(request sis.Request) {
+ 97 | request.SetNoLanguage()
97 | var builder strings.Builder
98 | fmt.Fprintf(&builder, "## Bible Versions\n")
99 | fmt.Fprintf(&builder, "### English\n")
100 | fmt.Fprintf(&builder, "=> /scriptures/christian/bible/esv/ ESV Bible\n")
101 | for _, version := range englishBibleVersions {
... | ...
163 | Tags: #bible #new #old #testament #septuagint #pentateuch
164 | `, builder.String()))
165 | })
166 |
167 | g.AddRoute("/scriptures/christian/bible/esv/", func(request sis.Request) {
+ 169 | request.SetLanguage("en-US")
168 | var builder strings.Builder
169 | for _, book := range asvBooks {
170 | fmt.Fprintf(&builder, "=> /scriptures/christian/bible/esv/%s/ %s\n", url.PathEscape(book.Name+" 1"), book.Name)
171 | }
172 |
... | ...
183 | Users may not copy or download more than 500 verses of the ESV Bible or more than one half of any book of the ESV Bible.
184 | `, builder.String()))
185 | })
186 |
187 | g.AddRoute("/scriptures/christian/bible/esv/:text", func(request sis.Request) {
+ 190 | request.SetLanguage("en-US")
188 | text := request.GetParam("text")
189 | resp := GetPassages(text)
190 | var builder strings.Builder
191 | for _, s := range resp.Passages {
192 | fmt.Fprintf(&builder, "%s", s)
... | ...
199 | })
200 |
201 | g.AddRoute("/scriptures/christian/bible/:id", func(request sis.Request) {
202 | versionId := request.GetParam("id")
203 | version := GetBibleVersion(versionId, apiKey)
+ 207 | request.SetLanguage(version.Language.Id)
204 | books := GetBooks(versionId, apiKey)
205 | var builder strings.Builder
206 | for _, book := range books {
207 | fmt.Fprintf(&builder, "=> /scriptures/christian/bible/%s/%s/ %s\n", versionId, book.Id, book.Name)
208 | }
... | ...
221 |
222 | g.AddRoute("/scriptures/christian/bible/:id/:book", func(request sis.Request) {
223 | versionId := request.GetParam("id")
224 | bookId := request.GetParam("book")
225 | version := GetBibleVersion(versionId, apiKey)
+ 230 | request.SetLanguage(version.Language.Id)
226 | book := GetBook(versionId, bookId, apiKey, true)
227 | var builder strings.Builder
228 | for _, chapter := range book.Chapters {
229 | fmt.Fprintf(&builder, "=> /scriptures/christian/bible/%s/chapter/%s/ Chapter %s\n", versionId, chapter.Id, chapter.Number)
230 | }
... | ...
242 |
243 | g.AddRoute("/scriptures/christian/bible/:id/chapter/:chapter", func(request sis.Request) {
244 | versionId := request.GetParam("id")
245 | chapterId := request.GetParam("chapter")
246 | version := GetBibleVersion(versionId, apiKey)
+ 252 | request.SetLanguage(version.Language.Id)
247 | chapter := GetChapter(versionId, chapterId, apiKey)
248 | var builder strings.Builder
249 | fmt.Fprintf(&builder, "%s", chapter.Content)
250 | /*for _, chapter := range book.Chapters {
251 | fmt.Fprintf(&builder, "=> /scriptures/christian/bible/%s/%s/%s Chapter %s\n", versionId, book.Id, chapter.Id, chapter.Number)
gemini/texts/islam/islam.go
... | ...
106 | }
107 |
108 | versionNames["arabic"] = "Qur'an"
109 |
110 | g.AddRoute("/scriptures/islam", func(request sis.Request) {
+ 111 | request.SetNoLanguage()
111 | var builder strings.Builder
112 | fmt.Fprintf(&builder, "## Qur'an Versions\n\n=> /scriptures/islam/quran/arabic/ Arabic\n")
113 | fmt.Fprintf(&builder, "### English\n")
114 | for _, version := range englishQuranVersions {
115 | if version.Identifier != "" {
... | ...
189 | g.AddRoute("/scriptures/islam/quran/:version/:surah", func(request sis.Request) {
190 | versionId := request.GetParam("version")
191 | surahNumber := request.GetParam("surah")
192 |
193 | surah := GetSurah(versionId, surahNumber)
+ 195 | request.SetLanguage(surah.Edition.Language)
194 |
195 | var builder strings.Builder
196 | for _, ayah := range surah.Ayahs {
197 | fmt.Fprintf(&builder, "[%d] %s\n", ayah.NumberInSurah, ayah.Text)
198 | }
gemini/texts/judaism/judaism.go
... | ...
23 |
24 | func HandleJewishTexts(g sis.ServerHandle) {
25 | //stripTags := bluemonday.StripTagsPolicy()
26 |
27 | index := GetFullIndex()
+ 28 | fmt.Printf("Index: %v\n", index)
28 | //indexMap := make(map[string]int)
29 |
30 | g.AddRoute("/scriptures/jewish/", func(request sis.Request) {
31 | query, err := request.Query()
32 | if err != nil {
gemini/texts/judaism/sefaria.go
... | ...
73 | if err != nil {
74 | fmt.Println(err)
75 | return []SefariaIndexCategoryOrText{}
76 | }
77 |
- 78 | req.Header.Set("User-Agent", "AuraGem")
+ 78 | req.Header.Set("User-Agent", "ScholasticDiversity")
+ 79 | req.Header.Set("accept", "application/json")
79 | res, getErr := spaceClient.Do(req)
80 | if getErr != nil {
81 | fmt.Println(err)
82 | return []SefariaIndexCategoryOrText{}
83 | }
... | ...
247 | _, filename, line, _ := runtime.Caller(0)
248 | fmt.Printf("%s:%d %s\n", filename, line, err)
249 | return SefariaText{}
250 | }
251 |
- 252 | req.Header.Set("User-Agent", "AuraGem")
+ 253 | req.Header.Set("User-Agent", "ScholasticDiversity")
+ 254 | req.Header.Set("accept", "application/json")
253 | res, getErr := spaceClient.Do(req)
254 | if getErr != nil {
255 | _, filename, line, _ := runtime.Caller(0)
256 | fmt.Printf("%s:%d %s\n", filename, line, getErr)
257 | return SefariaText{}
... | ...
389 | AnchorRefExpanded []string `json:"anchorRefExpanded"`
390 | SourceRef string `json:"sourceRef"`
391 | SourceHeRef string `json:"sourceHeRef"`
392 | AnchorVerse int `json:"anchorVerse"`
393 | SourceHasEn bool `json:"sourceHasEn"`
- 394 | CompDate int `json:"compDate"`
+ 396 | CompDate []int `json:"compDate"`
395 | ErrorMargin int `json:"errorMargin"`
396 | // AnchorVersion
397 | //sourceVersion
398 | DisplayedText SefariaHebrewEnglishText `json:"displayedText"`
399 | CommentaryNum float64 `json:"commentaryNum"` // Will have decimal if the commentary covers only a verse (a part of the given ref); will be whole number for number of commentaries on whole given ref
... | ...
416 | _, filename, line, _ := runtime.Caller(0)
417 | fmt.Printf("%s:%d %s\n", filename, line, err)
418 | return []SefariaTextLink{}
419 | }
420 |
- 421 | req.Header.Set("User-Agent", "AuraGem")
+ 423 | req.Header.Set("User-Agent", "ScholasticDiversity")
+ 424 | req.Header.Set("accept", "application/json")
422 | res, getErr := spaceClient.Do(req)
423 | if getErr != nil {
424 | _, filename, line, _ := runtime.Caller(0)
425 | fmt.Printf("%s:%d %s\n", filename, line, getErr)
426 | return []SefariaTextLink{}
... | ...
485 | if err != nil {
486 | fmt.Println(err)
487 | return SefariaCalendarResponse{}
488 | }
489 |
- 490 | req.Header.Set("User-Agent", "AuraGem")
+ 493 | req.Header.Set("User-Agent", "ScholasticDiversity")
491 | res, getErr := spaceClient.Do(req)
492 | if getErr != nil {
493 | fmt.Println(err)
494 | return SefariaCalendarResponse{}
495 | }
gemini/texts/judaism/sefaria_test.go (created)
+ 1 | package judaism
+ 2 |
+ 3 | import (
+ 4 | "fmt"
+ 5 | "testing"
+ 6 | )
+ 7 |
+ 8 | // Test the sefaria index
+ 9 | func TestSefariaIndex(t *testing.T) {
+ 10 | index := GetFullIndex()
+ 11 | fmt.Printf("index: %v\n", index)
+ 12 | if len(index) == 0 {
+ 13 | t.FailNow()
+ 14 | }
+ 15 | }
+ 16 |
+ 17 | func TestSefariaText(t *testing.T) {
+ 18 | text := GetText("Genesis 1", "en", "")
+ 19 | if text.Ref == "" {
+ 20 | t.FailNow()
+ 21 | }
+ 22 | }
+ 23 |
+ 24 | func TestSefariaTextLinks(t *testing.T) {
+ 25 | text := GetText("Genesis 1", "en", "")
+ 26 | links := GetLinks(text.Ref, "en", "")
+ 27 | if len(links) == 0 {
+ 28 | t.FailNow()
+ 29 | }
+ 30 | }
gemini/utils/atom.go
... | ...
12 | link string
13 | date time.Time
14 | title string
15 | }
16 |
- 17 | func GenerateAtomFrom(file string, domain string, baseurl string, authorName string, authorEmail string) string {
+ 17 | func GenerateAtomFrom(file string, domain string, baseurl string, authorName string, authorEmail string) (string, string, time.Time) {
18 | //ISO8601Layout := "2006-01-02T15:04:05Z0700"
19 | feedTitle := ""
20 | var posts []AtomPost
21 | last_updated, _ := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z")
22 |
... | ...
77 | `, html.EscapeString(post.title), post.link, post.link, post_date_string)
78 | }
79 |
80 | fmt.Fprintf(&builder, ``)
81 |
- 82 | return builder.String()
+ 82 | return builder.String(), feedTitle, last_updated
83 | }
gemini/weather.go
... | ...
13 | )
14 |
15 | var apiKey = config.WeatherApiKey
16 |
17 | func handleWeather(g sis.ServerHandle) {
+ 18 | publishDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-19T13:51:00", time.Local)
18 | g.AddRoute("/weather", func(request sis.Request) {
19 | request.Redirect("/weather/")
20 | })
21 | g.AddRoute("/weather/", func(request sis.Request) {
... | ...
17 | g.AddRoute("/weather", func(request sis.Request) {
18 | request.Redirect("/weather/")
19 | })
20 | g.AddRoute("/weather/", func(request sis.Request) {
+ 23 | request.SetScrollMetadataResponse(sis.ScrollMetadata{PublishDate: publishDate, UpdateDate: time.Now(), Language: "en", Abstract: "# Weather\n"})
+ 24 | if request.ScrollMetadataRequested {
+ 25 | request.SendAbstract("")
+ 26 | return
+ 27 | }
22 | iqAirResponse := getNearestLocation(request)
23 | request.Gemini(fmt.Sprintf(`# Weather for %s, %s, %s
24 |
25 | %s
26 |
gemini/youtube/utils.go
... | ...
52 |
53 | request.Gemini(fmt.Sprintf(template, builder.String()))
54 | }
55 |
56 | func getChannelPlaylists(request sis.Request, service *youtube.Service, channelId string, currentPage string) {
+ 57 | request.SetNoLanguage()
57 | template := `# Playlists for '%s'
58 |
59 | => /youtube/channel/%s/ ChannelPage
60 | %s`
61 |
... | ...
104 | //log.Fatalf("Error: %v", err)
105 | panic(err)
106 | }
107 |
108 | channel := response.Items[0]
+ 110 | request.SetLanguage(channel.Snippet.DefaultLanguage)
109 | uploadsPlaylistId := channel.ContentDetails.RelatedPlaylists.Uploads
110 | time.Sleep(time.Millisecond * 120)
111 |
112 | var call2 *youtube.PlaylistItemsListCall
113 | if currentPage != "" {
... | ...
151 | request.TemporaryFailure("Failed to get channel info.")
152 | return
153 | }
154 |
155 | channel := response.Items[0]
+ 158 | request.SetLanguage(channel.Snippet.DefaultLanguage)
156 | uploadsPlaylistId := channel.ContentDetails.RelatedPlaylists.Uploads
157 |
158 | time.Sleep(time.Millisecond * 120)
159 | call2 := service.PlaylistItems.List([]string{"id", "snippet"}).PlaylistId(uploadsPlaylistId).MaxResults(100) // TODO
160 | response2, err2 := call2.Do()
... | ...
189 | return
190 | }
191 |
192 | playlist := response_pl.Items[0]
193 | playlistTitle := playlist.Snippet.Title
+ 197 | request.SetLanguage(playlist.Snippet.DefaultLanguage)
194 |
195 | time.Sleep(time.Millisecond * 120)
196 | var call *youtube.PlaylistItemsListCall
197 | if currentPage != "" {
198 | call = service.PlaylistItems.List([]string{"id", "snippet"}).PlaylistId(playlistId).MaxResults(50).PageToken(currentPage)
gemini/youtube/youtube.go
... | ...
12 | "strings"
13 | "time"
14 |
15 | //"log"
16 |
- 17 | ytd "github.com/clseibold/youtube/v2"
+ 17 | ytd "github.com/kkdai/youtube/v2"
18 | "gitlab.com/clseibold/auragem_sis/config"
19 | sis "gitlab.com/clseibold/smallnetinformationservices"
20 | "google.golang.org/api/option"
21 | "google.golang.org/api/youtube/v3"
22 | )
... | ...
53 | handleChannelPage(s, service)
54 | handlePlaylistPage(s, service)
55 | }
56 |
57 | func indexRoute(request sis.Request) {
+ 58 | creationDate, _ := time.ParseInLocation(time.RFC3339, "2024-03-17T11:57:00", time.Local)
+ 59 | abstract := "#AuraGem YouTube Proxy\n\nProxies YouTube to Scroll/Gemini. Lets you search and download videos and playlists.\n"
+ 60 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: "Christian Lee Seibold", PublishDate: creationDate.UTC(), UpdateDate: creationDate.UTC(), Language: "en", Abstract: abstract})
+ 61 | if request.ScrollMetadataRequested {
+ 62 | request.Scroll(abstract)
+ 63 | return
+ 64 | }
+ 65 |
58 | request.Gemini("# AuraGem YouTube Proxy\n\nWelcome to the AuraGem YouTube Proxy!\n\n")
59 | request.PromptLine("/youtube/search/", "Search")
60 | request.Gemini("=> / AuraGem Home\n")
61 | request.Gemini("=> gemini://kwiecien.us/gemcast/20210425.gmi See This Proxy Featured on Gemini Radio\n")
62 | }
... | ...
60 | request.Gemini("=> / AuraGem Home\n")
61 | request.Gemini("=> gemini://kwiecien.us/gemcast/20210425.gmi See This Proxy Featured on Gemini Radio\n")
62 | }
63 | func getSearchRouteFunc(service *youtube.Service) sis.RequestHandler {
64 | return func(request sis.Request) {
+ 73 | request.SetNoLanguage()
65 | query, err := request.RawQuery()
66 | if err != nil {
67 | request.TemporaryFailure(err.Error())
68 | return
69 | }
... | ...
75 | if err != nil {
76 | request.TemporaryFailure(err.Error())
77 | return
78 | }
79 |
+ 89 | abstract := fmt.Sprintf("# AuraGem YouTube Proxy Search - Query %s\n", rawQuery)
+ 90 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Language: "en", Abstract: abstract})
+ 91 | if request.ScrollMetadataRequested {
+ 92 | request.Scroll(abstract)
+ 93 | return
+ 94 | }
+ 95 |
80 | page := request.GetParam("page")
81 | if page == "" {
82 | searchYoutube(request, service, query, rawQuery, "")
83 | } else {
84 | searchYoutube(request, service, query, rawQuery, page)
... | ...
88 | }
89 |
90 | func getVideoPageRouteFunc(service *youtube.Service) sis.RequestHandler {
91 | return func(request sis.Request) {
92 | id := request.GetParam("id")
- 93 | call := service.Videos.List([]string{"id", "snippet"}).Id(id).MaxResults(1)
+ 109 | call := service.Videos.List([]string{"id", "snippet", "status"}).Id(id).MaxResults(1)
94 | response, err := call.Do()
95 | if err != nil {
96 | //log.Fatalf("Error: %v", err) // TODO
97 | panic(err)
98 | }
... | ...
99 |
100 | if len(response.Items) == 0 {
101 | request.TemporaryFailure("Video not found.")
102 | return
103 | }
- 104 | video := response.Items[0] // TODO: Error if video is not found
+ 120 | video := response.Items[0]
+ 121 |
+ 122 | lang := request.Server.DefaultLanguage()
+ 123 | if video.Snippet.DefaultLanguage != "" {
+ 124 | lang = video.Snippet.DefaultLanguage
+ 125 | }
+ 126 | publishDate := video.Snippet.PublishedAt
+ 127 | if video.Status.PrivacyStatus == "private" {
+ 128 | publishDate = video.Status.PublishAt
+ 129 | }
+ 130 | publishDateParsed, _ := time.Parse(time.RFC3339, publishDate)
+ 131 | abstract := fmt.Sprintf("# Video - %s\n%s\n", html.UnescapeString(video.Snippet.Title), html.UnescapeString(video.Snippet.Description))
+ 132 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: html.UnescapeString(video.Snippet.ChannelTitle), PublishDate: publishDateParsed.UTC(), UpdateDate: publishDateParsed.UTC(), Language: lang, Abstract: abstract})
+ 133 | if request.ScrollMetadataRequested {
+ 134 | request.Scroll(abstract)
+ 135 | return
+ 136 | }
105 |
106 | //video.ContentDetails.RegionRestriction.Allowed
107 |
108 | //video.ContentDetails.Definition
109 |
... | ...
200 | request.TemporaryFailure("Error: Couldn't find video. %s\n", err.Error())
201 | return
202 | }
203 |
204 | time.Sleep(time.Millisecond * 120)
- 205 | transcript, err := client.GetTranscript(video, "en")
- 206 | if err != nil {
+ 237 |
+ 238 | // Go through each requested language and try to find a transcript for them. Otherwise, fallback to english.
+ 239 | // If still no transcript found, then error out.
+ 240 | requestedLanguages := append(request.ScrollRequestedLanguages, "en") // Append fallback language
+ 241 | var transcript ytd.VideoTranscript
+ 242 | var found bool = false
+ 243 | for _, lang := range requestedLanguages {
+ 244 | var err error
+ 245 | transcript, err = client.GetTranscript(video, lang)
+ 246 | if err == nil {
+ 247 | request.SetLanguage(lang)
+ 248 | found = true
+ 249 | break
+ 250 | }
+ 251 | }
+ 252 | if !found {
207 | request.TemporaryFailure("Video doesn't have a transcript.\n")
208 | return
209 | }
210 |
211 | request.Gemini(transcript.String())
... | ...
246 | var captionFound ytd.CaptionTrack
247 | for _, caption := range video.CaptionTracks {
248 | if caption.Kind == kind && caption.LanguageCode == lang {
249 | captionFound = caption
250 | foundCaption = true
+ 297 | request.SetLanguage(caption.LanguageCode)
251 | break
252 | }
253 | }
254 | if !foundCaption {
255 | request.TemporaryFailure("Caption not found.")
... | ...
370 | return
371 | //return c.Gemini("Error: Video with Audio Not Found.\n%v", err) // TODO: Do different thing?
372 | }
373 | }
374 |
+ 422 | // Handle Scroll protocol Metadata
+ 423 | abstract := fmt.Sprintf("# %s\n%s\n", video.Title, video.Description)
+ 424 | // TODO: The language should be a BCP47 string
+ 425 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: video.Author, PublishDate: video.PublishDate.UTC(), Language: format.LanguageDisplayName(), Abstract: abstract})
+ 426 | if request.ScrollMetadataRequested {
+ 427 | request.Scroll(abstract)
+ 428 | return
+ 429 | }
+ 430 |
375 | //format := video.Formats.AudioChannels(2).FindByQuality("hd1080")
376 | //.DownloadSeparatedStreams(ctx, "", video, "hd1080", "mp4")
377 | //resp, err := client.GetStream(video, format)
378 |
379 | rc, _, err := client.GetStream(video, format)
... | ...
380 | if err != nil {
381 | request.TemporaryFailure("Video not found.\n")
382 | return
383 | //return c.Gemini("Error: Video Not Found\n%v", err)
384 | }
- 385 | request.StreamBuffer(format.MimeType, rc, make([]byte, 2*1024*1024)) // 2 MB Buffer
+ 441 | request.StreamBuffer(format.MimeType, rc, make([]byte, 1*1024*1024)) // 1 MB Buffer
386 | //err2 := c.Stream(format.MimeType, rc)
387 | rc.Close()
388 |
389 | //url, err := client.GetStreamURL(video, format)
390 |
... | ...
415 | //log.Fatalf("Error: %v", err) // TODO
416 | panic(err)
417 | }
418 |
419 | channel := response.Items[0]
+ 476 |
+ 477 | // Handle Scroll Protocol Metadata
+ 478 | abstract := fmt.Sprintf("# Channel: %s\n%s\n", html.UnescapeString(channel.Snippet.Title), html.UnescapeString(channel.Snippet.Description))
+ 479 | request.SetScrollMetadataResponse(sis.ScrollMetadata{Author: html.UnescapeString(channel.Snippet.Title), Language: channel.Snippet.DefaultLanguage, Abstract: abstract})
+ 480 | if request.ScrollMetadataRequested {
+ 481 | request.Scroll(abstract)
+ 482 | return
+ 483 | }
420 |
421 | request.Gemini(fmt.Sprintf(template, html.UnescapeString(channel.Snippet.Title), channel.Id, channel.Id, channel.Id, channel.Id, html.UnescapeString(channel.Snippet.Description), channel.Id))
422 | })
423 |
424 | // Channel Playlists
go.mod
... | ...
1 | module gitlab.com/clseibold/auragem_sis
2 |
3 | go 1.22.0
4 |
5 | require (
- 6 | github.com/clseibold/youtube/v2 v2.10.2-0.20240307001954-15aa838690ca
7 | github.com/dhowden/tag v0.0.0-20240122214204-713ab0e94639
8 | github.com/efarrer/iothrottler v0.0.3
9 | github.com/gammazero/deque v0.2.1
10 | github.com/go-stack/stack v1.8.1
11 | github.com/google/go-github/v60 v60.0.0
... | ...
8 | github.com/efarrer/iothrottler v0.0.3
9 | github.com/gammazero/deque v0.2.1
10 | github.com/go-stack/stack v1.8.1
11 | github.com/google/go-github/v60 v60.0.0
12 | github.com/juju/ratelimit v1.0.2
+ 12 | github.com/kkdai/youtube/v2 v2.10.1
13 | github.com/krayzpipes/cronticker v0.0.1
14 | github.com/nakagami/firebirdsql v0.9.8
15 | github.com/rs/zerolog v1.32.0
16 | github.com/spf13/cobra v1.8.0
... | ...
12 | github.com/krayzpipes/cronticker v0.0.1
13 | github.com/nakagami/firebirdsql v0.9.8
14 | github.com/rs/zerolog v1.32.0
15 | github.com/spf13/cobra v1.8.0
- 17 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240305011400-a0904eef49b4
- 18 | golang.org/x/net v0.21.0
- 19 | golang.org/x/oauth2 v0.17.0
+ 17 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240320064032-f24b54ba1726
+ 18 | golang.org/x/net v0.22.0
+ 19 | golang.org/x/oauth2 v0.18.0
20 | golang.org/x/text v0.14.0
21 | golang.org/x/time v0.5.0
... | ...
17 | golang.org/x/text v0.14.0
18 | golang.org/x/time v0.5.0
- 22 | google.golang.org/api v0.167.0
+ 22 | google.golang.org/api v0.170.0
23 | )
24 |
25 | require (
26 | cloud.google.com/go/compute v1.23.4 // indirect
27 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
... | ...
23 | )
24 |
25 | require (
26 | cloud.google.com/go/compute v1.23.4 // indirect
27 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ 28 | git.sr.ht/~adnano/go-gemini v0.2.4 // indirect
28 | github.com/bitly/go-simplejson v0.5.1 // indirect
... | ...
24 | github.com/bitly/go-simplejson v0.5.1 // indirect
- 29 | github.com/clseibold/go-gemini v0.0.0-20240226215632-c39755b92b21 // indirect
- 30 | github.com/dlclark/regexp2 v1.10.0 // indirect
- 31 | github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d // indirect
+ 30 | github.com/clseibold/go-gemini v0.0.0-20240314051634-436d3e54df5c // indirect
+ 31 | github.com/djherbis/times v1.6.0 // indirect
+ 32 | github.com/dlclark/regexp2 v1.11.0 // indirect
+ 33 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 // indirect
32 | github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb // indirect
33 | github.com/felixge/httpsnoop v1.0.4 // indirect
34 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
35 | github.com/go-logr/logr v1.4.1 // indirect
36 | github.com/go-logr/stdr v1.2.2 // indirect
... | ...
36 | github.com/go-logr/stdr v1.2.2 // indirect
37 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
39 | github.com/golang/protobuf v1.5.3 // indirect
40 | github.com/google/go-querystring v1.1.0 // indirect
- 41 | github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
+ 43 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
42 | github.com/google/s2a-go v0.1.7 // indirect
43 | github.com/google/uuid v1.6.0 // indirect
44 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
... | ...
40 | github.com/google/s2a-go v0.1.7 // indirect
41 | github.com/google/uuid v1.6.0 // indirect
42 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
- 45 | github.com/googleapis/gax-go/v2 v2.12.1 // indirect
+ 47 | github.com/googleapis/gax-go/v2 v2.12.2 // indirect
46 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
47 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
... | ...
43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
44 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
- 48 | github.com/kkdai/youtube/v2 v2.10.0 // indirect
49 | github.com/mattn/go-colorable v0.1.13 // indirect
50 | github.com/mattn/go-isatty v0.0.19 // indirect
51 | github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
52 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
... | ...
48 | github.com/mattn/go-colorable v0.1.13 // indirect
49 | github.com/mattn/go-isatty v0.0.19 // indirect
50 | github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
51 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
+ 54 | github.com/rivo/uniseg v0.4.7 // indirect
53 | github.com/robfig/cron/v3 v3.0.1 // indirect
54 | github.com/shopspring/decimal v1.2.0 // indirect
55 | github.com/spf13/pflag v1.0.5 // indirect
56 | github.com/warpfork/go-fsx v0.4.0 // indirect
57 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
... | ...
54 | github.com/shopspring/decimal v1.2.0 // indirect
55 | github.com/spf13/pflag v1.0.5 // indirect
56 | github.com/warpfork/go-fsx v0.4.0 // indirect
57 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
58 | go.opencensus.io v0.24.0 // indirect
- 59 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect
- 60 | go.opentelemetry.io/otel v1.23.0 // indirect
- 61 | go.opentelemetry.io/otel/metric v1.23.0 // indirect
- 62 | go.opentelemetry.io/otel/trace v1.23.0 // indirect
- 63 | golang.org/x/crypto v0.19.0 // indirect
+ 61 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ 62 | go.opentelemetry.io/otel v1.24.0 // indirect
+ 63 | go.opentelemetry.io/otel/metric v1.24.0 // indirect
+ 64 | go.opentelemetry.io/otel/trace v1.24.0 // indirect
+ 65 | golang.org/x/crypto v0.21.0 // indirect
64 | golang.org/x/sync v0.6.0 // indirect
... | ...
60 | golang.org/x/sync v0.6.0 // indirect
- 65 | golang.org/x/sys v0.17.0 // indirect
+ 67 | golang.org/x/sys v0.18.0 // indirect
66 | google.golang.org/appengine v1.6.8 // indirect
... | ...
62 | google.golang.org/appengine v1.6.8 // indirect
- 67 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect
- 68 | google.golang.org/grpc v1.61.1 // indirect
- 69 | google.golang.org/protobuf v1.32.0 // indirect
+ 69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
+ 70 | google.golang.org/grpc v1.62.1 // indirect
+ 71 | google.golang.org/protobuf v1.33.0 // indirect
70 | modernc.org/mathutil v1.4.2-0.20220822142738-b13e5b564332 // indirect
71 | )
72 |
73 | replace github.com/dhowden/tag => github.com/clseibold/tag v0.0.0-20230917192755-2c2210e229df
go.sum
... | ...
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw=
3 | cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI=
4 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
5 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+ 6 | git.sr.ht/~adnano/go-gemini v0.2.4 h1:L23qUmAAq+rwuIjOLFoJpiFq0BPK+AqhyGAqJ2O3JMU=
+ 7 | git.sr.ht/~adnano/go-gemini v0.2.4/go.mod h1:GKBptE2mvJYtgVI60gXF8N7Z+JgrCkNfW4GgoR8h008=
6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
7 | github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
8 | github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
9 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
10 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
... | ...
11 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
12 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
14 | github.com/clseibold/go-gemini v0.0.0-20240226215632-c39755b92b21 h1:yIuvR+yzYAaOKzq9uM331p+IaFYYrecrAWEktJK4oAs=
15 | github.com/clseibold/go-gemini v0.0.0-20240226215632-c39755b92b21/go.mod h1:OS3TRybkU3yIFETGhQfxm4Xhx/zGg1VKdpMUCQWEwuo=
+ 18 | github.com/clseibold/go-gemini v0.0.0-20240314051634-436d3e54df5c h1:mPIVyUzsxB81KBda3gG/Y3qHfNvoGrrRcON565KEVos=
+ 19 | github.com/clseibold/go-gemini v0.0.0-20240314051634-436d3e54df5c/go.mod h1:OS3TRybkU3yIFETGhQfxm4Xhx/zGg1VKdpMUCQWEwuo=
16 | github.com/clseibold/tag v0.0.0-20230917192755-2c2210e229df h1:Z1tgK6mztKE317wx2dE+1eJYN53EZN3vVZjbsiwyXac=
17 | github.com/clseibold/tag v0.0.0-20230917192755-2c2210e229df/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
... | ...
13 | github.com/clseibold/tag v0.0.0-20230917192755-2c2210e229df h1:Z1tgK6mztKE317wx2dE+1eJYN53EZN3vVZjbsiwyXac=
14 | github.com/clseibold/tag v0.0.0-20230917192755-2c2210e229df/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
- 18 | github.com/clseibold/youtube/v2 v2.10.2-0.20240307001954-15aa838690ca h1:Vx22Z/uufkF5LcKcRKwktNt98LRPE8B8TtEFUEHpkQk=
- 19 | github.com/clseibold/youtube/v2 v2.10.2-0.20240307001954-15aa838690ca/go.mod h1:+DiJe9yNKHwcTAFY95kx9BfFvfBC4bBacIvzM7gHfyw=
20 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
21 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
22 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
23 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
... | ...
23 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+ 30 | github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
+ 31 | github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
28 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
29 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
... | ...
25 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
26 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
- 30 | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
- 31 | github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+ 34 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+ 35 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
32 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
... | ...
28 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
- 33 | github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
- 34 | github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
+ 37 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM=
+ 38 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
35 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
36 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
37 | github.com/efarrer/iothrottler v0.0.3 h1:6m8eKBQ1ouigjXQoBxwEWz12fUGGYfYppEJVcyZFcYg=
38 | github.com/efarrer/iothrottler v0.0.3/go.mod h1:zGWF5N0NKSCskcPFytDAFwI121DdU/NfW4XOjpTR+ys=
39 | github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb h1:ioQwBmKdOCpMVS/bDaESqNWXIE/aw4+gsVtysCGMWZ4=
... | ...
89 | github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
90 | github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
91 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
92 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
93 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
- 94 | github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
- 95 | github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+ 98 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
+ 99 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
96 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
97 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
98 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
99 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
100 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
... | ...
98 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
99 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
100 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
101 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
102 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
- 103 | github.com/googleapis/gax-go/v2 v2.12.1 h1:9F8GV9r9ztXyAi00gsMQHNoF51xPZm8uj1dpYt2ZETM=
- 104 | github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
+ 107 | github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=
+ 108 | github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
105 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
106 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
107 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
108 | github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
109 | github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
... | ...
107 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
108 | github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
109 | github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
110 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
111 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
- 112 | github.com/kkdai/youtube/v2 v2.10.0 h1:s8gSWo3AxIafK560XwDVnha9aPXp3N2HQAh1x81R5Og=
- 113 | github.com/kkdai/youtube/v2 v2.10.0/go.mod h1:H5MLUXiXYuovcEhQT/uZf7BC/syIbAJlDKCDsG+WDsU=
+ 116 | github.com/kkdai/youtube/v2 v2.10.1 h1:jdPho4R7VxWoRi9Wx4ULMq4+hlzSVOXxh4Zh83f2F9M=
+ 117 | github.com/kkdai/youtube/v2 v2.10.1/go.mod h1:qL8JZv7Q1IoDs4nnaL51o/hmITXEIvyCIXopB0oqgVM=
114 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
115 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
116 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
117 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
118 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
... | ...
135 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
136 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
137 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
138 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
139 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+ 144 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+ 145 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
140 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
141 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
142 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
... | ...
138 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
139 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
140 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
- 143 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
- 144 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+ 149 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+ 150 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
145 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
146 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
147 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
148 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
149 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
... | ...
156 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
157 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
158 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
159 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
160 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
- 161 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
- 162 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+ 167 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+ 168 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
163 | github.com/warpfork/go-fsx v0.4.0 h1:mlSH89UOECT5+NdRo8gPaE92Pm1xvt6cbzGkFa4QcsA=
164 | github.com/warpfork/go-fsx v0.4.0/go.mod h1:oTACCMj+Zle+vgVa5SAhGAh7WksYpLgGUCKEAVc+xPg=
165 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
... | ...
161 | github.com/warpfork/go-fsx v0.4.0 h1:mlSH89UOECT5+NdRo8gPaE92Pm1xvt6cbzGkFa4QcsA=
162 | github.com/warpfork/go-fsx v0.4.0/go.mod h1:oTACCMj+Zle+vgVa5SAhGAh7WksYpLgGUCKEAVc+xPg=
163 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
- 166 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240305011400-a0904eef49b4 h1:Jp8ZWO9rlMNhsihjej85FynnOC6KBclNpPJ06IFYMHo=
- 167 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240305011400-a0904eef49b4/go.mod h1:+xMppp52ZAhfo5QdXvSgdtH1bZgQ8KIHz5cFYu5qhOc=
+ 172 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317144711-f0ccd0a58ff7 h1:0hbVQ8zW0/d0IjNFa38nQOjoOO/oH4z9+rHtsUD5B9o=
+ 173 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317144711-f0ccd0a58ff7/go.mod h1:Yw95GU1dH4dSW4pkXYLq6WJsWWmKrASHOowgYJGq7H8=
+ 174 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317181933-52fec0a82b28 h1:ToQ89sKdGiYBnto5z7w9ZDZ+OH6cTeaPRYNUCCTfFcU=
+ 175 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317181933-52fec0a82b28/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 176 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317183716-37387665420b h1:DA77p8oL52LjErtXFTaAQPynqiJWK6N+Sv6p80VzYlg=
+ 177 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317183716-37387665420b/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 178 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317191105-fd6ee397c1ba h1:nK/syXQ7o3ej3iSB550wK+tiRTnjo+Sdj9LYccR6WL8=
+ 179 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317191105-fd6ee397c1ba/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 180 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317193605-02e256eeda24 h1:uAyxd1IFuNaW24ULEtynTBEaYJDC57ilObNGyPfqybo=
+ 181 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317193605-02e256eeda24/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 182 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317194928-e625dd753f9e h1:GWwNshXrZidLCkFlAKmEgekNwruiTqFJQZG5udXJXEg=
+ 183 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240317194928-e625dd753f9e/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 184 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318085521-879434868c5c h1:d/NxEoiml9BioLHBlfSBmUWL7RxG11UD5RH6CuiHbEI=
+ 185 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318085521-879434868c5c/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 186 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318092405-9b42d9c3d84e h1:7Dnk5nNYp5ICu1LVL5xiHGQcpAEXl0K402ZKRNVHPew=
+ 187 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318092405-9b42d9c3d84e/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 188 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318143605-d22eece89e44 h1:BKQXTIfQrEqz77YoMgpEOIlSAQy43hdV0YMSYtw2xRs=
+ 189 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240318143605-d22eece89e44/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 190 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319063835-6db1965b8625 h1:z61O0TlaIgRpjEYCtX/OVNBXNDjflF0onZmjFb2xKNE=
+ 191 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319063835-6db1965b8625/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 192 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319064654-23fc12590aa0 h1:OEdK16A53nGzGTGTH6I7JsB5c6YyXi1+tjUS2sswFbY=
+ 193 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319064654-23fc12590aa0/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 194 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319065042-2e7069caa365 h1:v6t2kLP3ngDS5HJ8798AileESSQ4SsZTNB8NCrg13mw=
+ 195 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319065042-2e7069caa365/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 196 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319065757-a5a3c89c49cb h1:YF+20YT8NPkzTUWu6yw8G0A2ffxXpOdSYoBwJUSGX78=
+ 197 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319065757-a5a3c89c49cb/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 198 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319070402-52672e35a43d h1:P6YYmdcteOry+U/uoQDijA6gxRts5jxW+IH8cEgLKq0=
+ 199 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319070402-52672e35a43d/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 200 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319071335-baadaacaec1e h1:npPnPHMnCQk/bsblVaBCGAN2flJ8I6rC9V5eDqC8U88=
+ 201 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319071335-baadaacaec1e/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 202 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319072949-0fe917520b03 h1:35u88Nq8R2os8ytmdtnntxZ28wO7Ew1sbmbC+cUZfwk=
+ 203 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319072949-0fe917520b03/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 204 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319073106-bd4546200c8f h1:YDJomccihBtAZSdKVqm0KRK08o+x4r41llauKpCnnys=
+ 205 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319073106-bd4546200c8f/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 206 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319134410-f175db99b88f h1:rM1v/g4xWlA/zV2HCb5dD8dqT5/gjUtDPYZFq/c+tGA=
+ 207 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319134410-f175db99b88f/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 208 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319142407-07f9c5b41f6a h1:ifMB/kdYnG5h6BgaJ6/ccIScyygOCWJEI/adL+8cQQg=
+ 209 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319142407-07f9c5b41f6a/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 210 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319143712-0ca5ad5a0e7a h1:79hn8AWpRFze1B5mgUPDq7a3A90Qp9LDL/IN3BASrvk=
+ 211 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319143712-0ca5ad5a0e7a/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 212 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319183527-3327ed1a0ca6 h1:KzjfIgFUiTrizDdYncutr2LnALNcZuf7WITZb+ePlu8=
+ 213 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319183527-3327ed1a0ca6/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 214 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319184943-faeb15249c33 h1:1hPchnzZ9Y3QrzyHTmtYhKgwNu3Mnqo1/9QX6DqWnRo=
+ 215 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319184943-faeb15249c33/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 216 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319185246-45324425122c h1:U6Da/lQzv+ogO4chDHgPl2siSl/H+ojS73K9MLHmwdQ=
+ 217 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319185246-45324425122c/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 218 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319204445-c5e584afea65 h1:uUngDt0YQ/h0xXSVhjaFpWilEFwmxn+N4vVRRdB2GJY=
+ 219 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319204445-c5e584afea65/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 220 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319221841-193016e2caf6 h1:Xa++6d0InGvf+5CfFS1/3Tx+nfdva7wLNcJZQIDY8CQ=
+ 221 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319221841-193016e2caf6/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 222 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319223109-ff5cbb202bce h1:7JakuWo1ShIah6lMDUpa6tZ/z2jEMkbXGVN3HHA+bVs=
+ 223 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319223109-ff5cbb202bce/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 224 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319223418-ab5dacf81e27 h1:dKGvpbnuSAhVhRe/gGCENLd+/GSJPDIsWHmvSD2p6bo=
+ 225 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240319223418-ab5dacf81e27/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
+ 226 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240320064032-f24b54ba1726 h1:xyCFBnlrAqqh1JBnca5DFvTWKYSyxnemIbdqZiSIfLQ=
+ 227 | gitlab.com/clseibold/smallnetinformationservices v0.0.0-20240320064032-f24b54ba1726/go.mod h1:1K8FkDn3Ke4DQ+RTO5884rtfXjIquI5Foy5lfRTO9x4=
168 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
169 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
170 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
171 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
... | ...
167 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
168 | gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
169 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
170 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
- 172 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
- 173 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
- 174 | go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E=
- 175 | go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
- 176 | go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo=
- 177 | go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
- 178 | go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI=
- 179 | go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
+ 232 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+ 233 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+ 234 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+ 235 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+ 236 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+ 237 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+ 238 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+ 239 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
180 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
181 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
182 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
... | ...
178 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
179 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
180 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
- 183 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
- 184 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+ 243 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+ 244 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
185 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
186 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
187 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
188 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
189 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
... | ...
195 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
196 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
197 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
198 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
199 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
- 200 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
- 201 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+ 260 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+ 261 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
202 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
... | ...
198 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
- 203 | golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
- 204 | golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+ 263 | golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+ 264 | golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
205 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
206 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
207 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
208 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
209 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
... | ...
214 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
215 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
216 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
217 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
218 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+ 279 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
219 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
220 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
221 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
222 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
223 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
... | ...
219 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
220 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
221 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
222 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
223 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
- 224 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
- 225 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+ 285 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+ 286 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
226 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
227 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
228 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
229 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
230 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
... | ...
243 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
244 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
245 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
246 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
247 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
- 248 | google.golang.org/api v0.167.0 h1:CKHrQD1BLRii6xdkatBDXyKzM0mkawt2QP+H3LtPmSE=
- 249 | google.golang.org/api v0.167.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
+ 309 | google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
+ 310 | google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
250 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
251 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
252 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
253 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
254 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
... | ...
256 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
257 | google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU=
258 | google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M=
259 | google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A=
260 | google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I=
- 261 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY=
- 262 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk=
+ 322 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0=
+ 323 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
263 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
264 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
265 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
266 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
267 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
... | ...
263 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
264 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
265 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
266 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
267 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
- 268 | google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
- 269 | google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+ 329 | google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+ 330 | google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
270 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
271 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
272 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
273 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
274 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
... | ...
276 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
277 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
278 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
279 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
280 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
- 281 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
- 282 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+ 342 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+ 343 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
283 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
284 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
285 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
286 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
287 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
main.go
... | ...
-1 | package main
0 |
1 | import (
+ 4 | "log"
+ 5 | "os"
+ 6 | "runtime"
+ 7 | "runtime/pprof"
+ 8 |
4 | "gitlab.com/clseibold/auragem_sis/gemini"
5 | _ "gitlab.com/clseibold/auragem_sis/migration"
6 | //"io"
7 | //"io/ioutil"
8 | //"os"
... | ...
10 | //"github.com/pitr/gig"
11 | //"github.com/nakagami/firebirdsql"
12 | //_ "gitlab.com/clseibold/auragem_sis/migration"
13 | //"github.com/spf13/cobra"
14 | )
+ 20 |
+ 21 | const cpuprofile = "./cpu.prof"
+ 22 | const memprofile = "./mem.prof"
15 |
16 | func main() {
17 | /*conn, _ := sql.Open("firebirdsql", firebirdConnectionString)
18 | defer conn.Close()*/
19 |
... | ...
15 |
16 | func main() {
17 | /*conn, _ := sql.Open("firebirdsql", firebirdConnectionString)
18 | defer conn.Close()*/
19 |
+ 28 | if cpuprofile != "" {
+ 29 | f, err := os.Create(cpuprofile)
+ 30 | if err != nil {
+ 31 | log.Fatal("could not create CPU profile: ", err)
+ 32 | }
+ 33 | defer f.Close() // error handling omitted for example
+ 34 | if err := pprof.StartCPUProfile(f); err != nil {
+ 35 | log.Fatal("could not start CPU profile: ", err)
+ 36 | }
+ 37 | defer pprof.StopCPUProfile()
+ 38 | }
+ 39 |
20 | gemini.GeminiCommand.Execute()
... | ...
16 | gemini.GeminiCommand.Execute()
+ 41 |
+ 42 | if memprofile != "" {
+ 43 | f, err := os.Create(memprofile)
+ 44 | if err != nil {
+ 45 | log.Fatal("could not create memory profile: ", err)
+ 46 | }
+ 47 | defer f.Close() // error handling omitted for example
+ 48 | runtime.GC() // get up-to-date statistics
+ 49 | if err := pprof.WriteHeapProfile(f); err != nil {
+ 50 | log.Fatal("could not write memory profile: ", err)
+ 51 | }
+ 52 | }
21 | }