├── .travis.yml ├── LICENSE ├── README.md ├── acgsh.conf ├── db ├── db.go └── types.go ├── httpserver.go ├── main.go ├── rpc ├── rpc.go └── types.go ├── search └── search.go ├── timeline.go ├── types.go └── utils.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 acgshare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/acgshare/acgsh.svg?branch=master)](https://travis-ci.org/acgshare/acgsh) 2 | [![Join the chat at https://gitter.im/acgshare/acgsh](https://badges.gitter.im/acgshare/acgsh.svg)](https://gitter.im/acgshare/acgsh?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | 4 | ACGSH 5 | ================== 6 | 7 | 8 | 9 | 安装 10 | ---- 11 | 12 | 首先请安装好Golang和Twister,并保证Twister已同步完成,而且已建立一个用户账号。 13 | 14 | 下载编译ACGSH 15 | 16 | go get github.com/acgshare/acgsh 17 | 18 | 建立acgsh工作目录 19 | 20 | mkdir acgsh 21 | cd acgsh 22 | 23 | 下载acgsh_html目录 24 | 25 | git clone https://github.com/acgshare/acgsh_html.git 26 | 27 | 复制配置文件到acgsh工作目录 28 | 29 | cp $GOPATH/src/github.com/acgshare/acgsh/acgsh.conf ./ 30 | 31 | 修改acgsh.conf配置文件 32 | 33 | TwisterServer="http://user:pwd@127.0.0.1:28332" #请根据你的twister core设置修改 34 | TwisterUsername="your_twister_user_name" #修改这里为你建立的twister用户账号 35 | HttpServerPort = "8080" #http服务端口号 36 | 37 | 运行ACGSH 38 | 39 | acgsh 40 | 41 | acgsh会在当前目录下建立acgsh_bolt.db数据库文件和acgsh.bleve索引文件夹并同步你的twister账号follow的账号内容到数据库。 42 | 43 | 推荐使用[Supervisor](http://supervisord.org/) 来使acgsh在后台工作并监控acgsh工作状态。 44 | 45 | 46 | -------------------------------------------------------------------------------- /acgsh.conf: -------------------------------------------------------------------------------- 1 | # Twister core RPC server address 2 | TwisterServer="http://user:pwd@127.0.0.1:28332" 3 | 4 | TwisterUsername="your_twister_user_name" 5 | 6 | HttpServerPort = "8080" -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/boltdb/bolt" 8 | "log" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const max_post_id = 99999999 14 | 15 | var db *bolt.DB 16 | 17 | // itob returns an 8-byte big endian representation of v. 18 | func itob(v int) []byte { 19 | b := make([]byte, 8) 20 | binary.BigEndian.PutUint64(b, uint64(v)) 21 | return b 22 | } 23 | func Ui64tob(v uint64) []byte { 24 | b := make([]byte, 8) 25 | binary.BigEndian.PutUint64(b, v) 26 | return b 27 | } 28 | 29 | func Init() { 30 | var err error 31 | log.Println("Open DB: acgsh_bolt.db") 32 | db, err = bolt.Open("acgsh_bolt.db", 0600, &bolt.Options{Timeout: 1 * time.Second}) 33 | 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | //Initialise all buckets 39 | db.Update(func(tx *bolt.Tx) error { 40 | _, err := tx.CreateBucketIfNotExists([]byte("Posts")) 41 | if err != nil { 42 | log.Printf("DB create Posts bucket: %s", err) 43 | return err 44 | } 45 | _, err = tx.CreateBucketIfNotExists([]byte("Publishers")) 46 | if err != nil { 47 | log.Printf("DB create Publisher bucket: %s", err) 48 | return err 49 | } 50 | _, err = tx.CreateBucketIfNotExists([]byte("PublishersReplyPosts")) 51 | if err != nil { 52 | log.Printf("DB create PublishersReplyPosts bucket: %s", err) 53 | return err 54 | } 55 | _, err = tx.CreateBucketIfNotExists([]byte("PublishersPostsIndex")) 56 | if err != nil { 57 | log.Printf("DB create PublishersPostsIndex bucket: %s", err) 58 | return err 59 | } 60 | // Category of posts 61 | _, err = tx.CreateBucketIfNotExists([]byte("CategoryPostsIndex")) 62 | if err != nil { 63 | log.Printf("DB create CategoryPostsIndex bucket: %s", err) 64 | return err 65 | } 66 | _, err = tx.CreateBucketIfNotExists([]byte("NameKPostsIndex")) 67 | if err != nil { 68 | log.Printf("DB create NameKPostsIndex bucket: %s", err) 69 | return err 70 | } 71 | return nil 72 | }) 73 | 74 | // Check stats 75 | /* go func() { 76 | // Grab the initial stats. 77 | prev := db.Stats() 78 | 79 | for { 80 | // Wait for 10s. 81 | time.Sleep(10 * time.Second) 82 | 83 | // Grab the current stats and diff them. 84 | stats := db.Stats() 85 | diff := stats.Sub(&prev) 86 | 87 | // Encode stats to JSON and print to STDERR. 88 | json.NewEncoder(os.Stderr).Encode(diff) 89 | 90 | // Save stats for the next loop. 91 | prev = stats 92 | } 93 | }()*/ 94 | 95 | log.Println("DB initialised successfully.") 96 | } 97 | 98 | func Close() { 99 | 100 | db.Close() 101 | log.Println("DB closed.") 102 | 103 | } 104 | 105 | func AddPublishersIfNotExist(names *[]string) error { 106 | err := db.Update(func(tx *bolt.Tx) error { 107 | bucket := tx.Bucket([]byte("Publishers")) 108 | if bucket == nil { 109 | return fmt.Errorf("Bucket Publisher not found!") 110 | } 111 | 112 | for _, name := range *names { 113 | //fmt.Printf("Value [%d] is [%s]\n", index, name) 114 | v := bucket.Get([]byte(name)) 115 | if v == nil { 116 | err := bucket.Put([]byte(name), []byte{}) 117 | if err != nil { 118 | return err 119 | } 120 | } 121 | } 122 | 123 | return nil 124 | }) 125 | 126 | return err 127 | } 128 | 129 | type SyncData struct { 130 | Max int64 `json:"max"` 131 | Latest int64 `json:"latest"` 132 | Since int64 `json:"since"` 133 | } 134 | 135 | func newSyncData() *SyncData { 136 | return &SyncData{ 137 | Max: max_post_id, 138 | Latest: -1, 139 | Since: -1, 140 | } 141 | } 142 | 143 | func GetPublishers() (map[string]SyncData, error) { 144 | publishers := make(map[string]SyncData) 145 | err := db.View(func(tx *bolt.Tx) error { 146 | bucket := tx.Bucket([]byte("Publishers")) 147 | if bucket == nil { 148 | return fmt.Errorf("Bucket Publishers not found!") 149 | } 150 | 151 | bucket.ForEach(func(k, v []byte) error { 152 | //fmt.Printf("key=%s, value=%s\n", k, v) 153 | var sd *SyncData 154 | sd = newSyncData() 155 | if len(v) == 0 { 156 | publishers[string(k)] = *sd 157 | return nil 158 | } 159 | 160 | err := json.Unmarshal(v, sd) 161 | if err != nil { 162 | log.Println(err) 163 | log.Printf("Error: DB GetPublishers Unmarshal: %s", v) 164 | sd = newSyncData() 165 | publishers[string(k)] = *sd 166 | return nil 167 | } 168 | 169 | publishers[string(k)] = *sd 170 | return nil 171 | }) 172 | 173 | return nil 174 | }) 175 | return publishers, err 176 | } 177 | 178 | func UpdatePublishers(publishers *map[string]SyncData) error { 179 | err := db.Update(func(tx *bolt.Tx) error { 180 | bucket := tx.Bucket([]byte("Publishers")) 181 | if bucket == nil { 182 | return fmt.Errorf("Bucket Publisher not found!") 183 | } 184 | 185 | for name, sd := range *publishers { 186 | //fmt.Printf("Value [%d] is [%s]\n", index, name) 187 | jsonData, err := json.Marshal(sd) 188 | if err != nil { 189 | log.Println(err) 190 | log.Printf("Error: DB UpdatePublishers Marshal: %+v\n", sd) 191 | continue 192 | } 193 | //log.Printf("%s\n", jsonData) 194 | 195 | err = bucket.Put([]byte(name), jsonData) 196 | if err != nil { 197 | log.Println(err) 198 | log.Printf("Error: DB UpdatePublishers bucket.Put: %s : %s\n", name, jsonData) 199 | continue 200 | } 201 | } 202 | 203 | return nil 204 | }) 205 | 206 | return err 207 | } 208 | 209 | func DeletePublishers(names *[]string) error { 210 | err := db.Update(func(tx *bolt.Tx) error { 211 | /* bucket, err := tx.CreateBucketIfNotExists("fsef") 212 | if err != nil { 213 | return err 214 | } 215 | 216 | err = bucket.Put(key, value) 217 | if err != nil { 218 | return err 219 | }*/ 220 | return nil 221 | }) 222 | 223 | return err 224 | } 225 | 226 | func AddPosts(posts *ShPosts) { 227 | for _, sp := range *posts { 228 | _ = db.Update(func(tx *bolt.Tx) error { 229 | bucketP := tx.Bucket([]byte("Posts")) 230 | if bucketP == nil { 231 | return fmt.Errorf("Bucket Posts not found!") 232 | } 233 | bucketPPI := tx.Bucket([]byte("PublishersPostsIndex")) 234 | if bucketPPI == nil { 235 | return fmt.Errorf("Bucket PublishersPostsIndex not found!") 236 | } 237 | bucketCPI := tx.Bucket([]byte("CategoryPostsIndex")) 238 | if bucketCPI == nil { 239 | return fmt.Errorf("Bucket CategoryPostsIndex not found!") 240 | } 241 | bucketNKPI := tx.Bucket([]byte("NameKPostsIndex")) 242 | if bucketNKPI == nil { 243 | return fmt.Errorf("Bucket NameKPostsIndex not found!") 244 | } 245 | 246 | /////////////////////////////////////// 247 | // Put into Posts bucket 248 | category := getCategory(sp.Category) 249 | sp.Category = category 250 | //fmt.Printf("Value [%d] is [%s]\n", index, name) 251 | jsonData, err := json.Marshal(sp) 252 | if err != nil { 253 | log.Println(err) 254 | log.Printf("Error: DB addPosts Marshal: %+v\n", sp) 255 | return err 256 | } 257 | // log.Printf("%s\n", jsonData) 258 | 259 | id := Ui64tob(sp.Time) 260 | id = append(id, ":"...) 261 | id = append(id, sp.N...) 262 | id = append(id, ":"...) 263 | id = append(id, strconv.FormatInt(sp.K, 10)...) 264 | err = bucketP.Put(id, jsonData) 265 | if err != nil { 266 | log.Println(err) 267 | log.Printf("Error: DB addPosts bucket.Put: %s : %s\n", id, jsonData) 268 | return err 269 | } 270 | // fmt.Printf("Value [%s] is [%s]\n", bs, jsonData) 271 | 272 | /////////////////////////////////////// 273 | // Put into NameKPostsIndex bucket 274 | nk := []byte{} 275 | nk = append(nk, sp.N...) 276 | nk = append(nk, ":"...) 277 | nk = append(nk, strconv.FormatInt(sp.K, 10)...) 278 | err = bucketNKPI.Put(nk, id) 279 | if err != nil { 280 | log.Println(err) 281 | log.Printf("Error: DB addPosts NameKPostsIndex bucket.Put: %s : %s\n", nk, jsonData) 282 | return err 283 | } 284 | 285 | /////////////////////////////////////// 286 | // Put into PublishersPostsIndex bucket 287 | bucketPP, err := bucketPPI.CreateBucketIfNotExists([]byte(sp.N)) 288 | if err != nil { 289 | log.Printf("DB CreateBucketIfNotExists PublishersPostsIndex bucket: %s: %s\n", sp.N, err) 290 | return err 291 | } 292 | err = bucketPP.Put(id, []byte{}) 293 | if err != nil { 294 | log.Println(err) 295 | log.Printf("Error: DB AddPosts PublishersPostsIndex bucket.Put: %s : %s\n", sp.N, id) 296 | return err 297 | } 298 | /////////////////////////////////////// 299 | // Put into CategoryPostsIndex bucket 300 | 301 | bucketCP, err := bucketCPI.CreateBucketIfNotExists([]byte(category)) 302 | if err != nil { 303 | log.Printf("DB CreateBucketIfNotExists CategoryPostsIndex bucket: %s: %s\n", category, err) 304 | return err 305 | } 306 | err = bucketCP.Put(id, []byte{}) 307 | if err != nil { 308 | log.Println(err) 309 | log.Printf("Error: DB AddPosts CategoryPostsIndex bucket.Put: %s : %s %s\n", sp.Category, category, id) 310 | return err 311 | } 312 | return nil 313 | }) 314 | } 315 | 316 | } 317 | 318 | func getCategory(str string) string { 319 | /* 320 | 動畫 321 | 季度全集 322 | 漫畫 323 | 音樂 324 | 日劇 325 | RAW 326 | 遊戲 327 | 特攝 328 | 其他 329 | */ 330 | category := "其他" 331 | if str == "動畫" || str == "动画" { 332 | category = "動畫" 333 | } 334 | if str == "季度全集" || str == "季度全集" { 335 | category = "季度全集" 336 | } 337 | if str == "漫畫" || str == "漫画" { 338 | category = "漫畫" 339 | } 340 | if str == "音樂" || str == "音乐" || str == "動漫音樂" || str == "动漫音乐" { 341 | category = "音樂" 342 | } 343 | if str == "日劇" || str == "日剧" { 344 | category = "日劇" 345 | } 346 | if str == "RAW" || str == "RAW" { 347 | category = "RAW" 348 | } 349 | if str == "遊戲" || str == "游戏" { 350 | category = "遊戲" 351 | } 352 | if str == "特攝" || str == "特摄" { 353 | category = "特攝" 354 | } 355 | 356 | return category 357 | } 358 | 359 | // todo: Check if reply post was reply to acgsh post in DB. 360 | func AddPublishersReplyPosts(posts *ShPubReplyPosts) error { 361 | err := db.Update(func(tx *bolt.Tx) error { 362 | bucket := tx.Bucket([]byte("PublishersReplyPosts")) 363 | if bucket == nil { 364 | return fmt.Errorf("Bucket PublishersReplyPosts not found!") 365 | } 366 | 367 | for _, sp := range *posts { 368 | jsonData, err := json.Marshal(sp) 369 | if err != nil { 370 | log.Println(err) 371 | log.Printf("Error: DB AddPublishersReplyPosts Marshal: %+v\n", sp) 372 | continue 373 | } 374 | bs := []byte(sp.N) 375 | bs = append(bs, ":"...) 376 | bs = append(bs, strconv.FormatInt(sp.K, 10)...) 377 | 378 | newData := []byte{} 379 | v := bucket.Get(bs) 380 | if v == nil { 381 | newData = append(newData, "["...) 382 | } else if len(v) >= 2 { 383 | newData = append(newData, v[:(len(v)-1)]...) 384 | newData = append(newData, ","...) 385 | } else { 386 | newData = append(newData, "["...) 387 | } 388 | newData = append(newData, jsonData...) 389 | newData = append(newData, "]"...) 390 | 391 | err = bucket.Put(bs, newData) 392 | if err != nil { 393 | log.Println(err) 394 | log.Printf("Error: DB AddPublishersReplyPosts bucket.Put: %s : %s\n", bs, newData) 395 | continue 396 | } 397 | } 398 | 399 | return nil 400 | }) 401 | 402 | return err 403 | } 404 | 405 | func GetPosts(idx, n uint) ([]byte, error) { 406 | if n < 1 { 407 | return []byte{}, fmt.Errorf("Invalid n") 408 | } 409 | buf := []byte("[") 410 | err := db.View(func(tx *bolt.Tx) error { 411 | bucket := tx.Bucket([]byte("Posts")) 412 | if bucket == nil { 413 | return fmt.Errorf("Bucket Posts not found!") 414 | } 415 | 416 | cur := bucket.Cursor() 417 | 418 | comma := []byte(",") 419 | i := uint(0) 420 | for k, v := cur.Last(); k != nil; k, v = cur.Prev() { 421 | if i >= idx { 422 | if i != idx { 423 | buf = append(buf, comma...) 424 | } 425 | buf = append(buf, v...) 426 | } 427 | i = i + 1 428 | if i >= idx+n { 429 | break 430 | } 431 | } 432 | 433 | return nil 434 | }) 435 | buf = append(buf, "]"...) 436 | return buf, err 437 | } 438 | func GetPostsWithIds(ids [][]byte) ([]byte, error) { 439 | 440 | if len(ids) == 0 { 441 | return []byte("[]"), nil 442 | } 443 | 444 | buf := []byte("[") 445 | err := db.View(func(tx *bolt.Tx) error { 446 | bucket := tx.Bucket([]byte("Posts")) 447 | if bucket == nil { 448 | return fmt.Errorf("Bucket Posts not found!") 449 | } 450 | 451 | comma := []byte(",") 452 | 453 | for _, id := range ids { 454 | value := bucket.Get(id) 455 | if value == nil { 456 | continue 457 | } 458 | buf = append(buf, value...) 459 | buf = append(buf, comma...) 460 | } 461 | 462 | return nil 463 | }) 464 | buf = buf[:len(buf)-1] 465 | buf = append(buf, "]"...) 466 | 467 | return buf, err 468 | } 469 | 470 | func GetCategoryPosts(category string, idx, n uint) ([]byte, error) { 471 | if n < 1 { 472 | return []byte{}, fmt.Errorf("Invalid n") 473 | } 474 | bufIds := [][]byte{} 475 | err := db.View(func(tx *bolt.Tx) error { 476 | bucketCPI := tx.Bucket([]byte("CategoryPostsIndex")) 477 | if bucketCPI == nil { 478 | return fmt.Errorf("Bucket CategoryPostsIndex not found!") 479 | } 480 | 481 | bucketCP := bucketCPI.Bucket([]byte(category)) 482 | if bucketCP == nil { 483 | //return fmt.Errorf("Bucket CategoryPostsIndex %s not found!",category) 484 | return nil 485 | } 486 | 487 | cur := bucketCP.Cursor() 488 | 489 | i := uint(0) 490 | for k, _ := cur.Last(); k != nil; k, _ = cur.Prev() { 491 | if i >= idx { 492 | bufIds = append(bufIds, k) 493 | } 494 | i = i + 1 495 | if i >= idx+n { 496 | break 497 | } 498 | } 499 | 500 | return nil 501 | }) 502 | if err != nil { 503 | return []byte{}, err 504 | } 505 | 506 | return GetPostsWithIds(bufIds) 507 | 508 | } 509 | func GetPubPosts(publisher string, idx, n uint) ([]byte, error) { 510 | if n < 1 { 511 | return []byte{}, fmt.Errorf("Invalid n") 512 | } 513 | bufIds := [][]byte{} 514 | err := db.View(func(tx *bolt.Tx) error { 515 | bucketPPI := tx.Bucket([]byte("PublishersPostsIndex")) 516 | if bucketPPI == nil { 517 | return fmt.Errorf("Bucket PublishersPostsIndex not found!") 518 | } 519 | 520 | bucketPP := bucketPPI.Bucket([]byte(publisher)) 521 | if bucketPP == nil { 522 | //return fmt.Errorf("Bucket PublishersPostsIndex %s not found!",publisher) 523 | return nil 524 | } 525 | 526 | cur := bucketPP.Cursor() 527 | 528 | i := uint(0) 529 | for k, _ := cur.Last(); k != nil; k, _ = cur.Prev() { 530 | if i >= idx { 531 | bufIds = append(bufIds, k) 532 | } 533 | i = i + 1 534 | if i >= idx+n { 535 | break 536 | } 537 | } 538 | 539 | return nil 540 | }) 541 | if err != nil { 542 | return []byte{}, err 543 | } 544 | return GetPostsWithIds(bufIds) 545 | } 546 | 547 | func GetPublishersReplyPosts(name, k string) ([]byte, error) { 548 | var data []byte 549 | err := db.View(func(tx *bolt.Tx) error { 550 | bucket := tx.Bucket([]byte("PublishersReplyPosts")) 551 | if bucket == nil { 552 | return fmt.Errorf("Bucket PublishersReplyPosts not found!") 553 | } 554 | 555 | bs := []byte(name) 556 | bs = append(bs, ":"...) 557 | bs = append(bs, k...) 558 | value := bucket.Get(bs) 559 | if value == nil { 560 | value = []byte("[]") 561 | } 562 | 563 | data = make([]byte, len(value)) 564 | copy(data, value) 565 | 566 | return nil 567 | }) 568 | 569 | return data, err 570 | } 571 | -------------------------------------------------------------------------------- /db/types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type ShPubReplyPost struct { 4 | //SigUserpost string `json:"sig_userpost"` 5 | Msg string `json:"msg"` 6 | K int64 `json:"k"` 7 | Lastk int64 `json:"lastk"` 8 | N string `json:"n"` 9 | //Height int64 `json:"height"` 10 | Time uint64 `json:"time"` 11 | } 12 | type ShPubReplyPosts []ShPubReplyPost 13 | 14 | type ShPost struct { 15 | //SigUserpost string `json:"sig_userpost"` 16 | Msg string `json:"msg"` 17 | N string `json:"n"` 18 | K int64 `json:"k"` 19 | Lastk int64 `json:"lastk"` 20 | //Height int64 `json:"height"` 21 | 22 | Time uint64 `json:"time"` 23 | Category string `json:"category"` 24 | Title string `json:"title"` 25 | Magnet string `json:"magnet"` 26 | Size uint64 `json:"size"` 27 | Team string `json:"team"` 28 | } 29 | 30 | type ShPosts []ShPost 31 | -------------------------------------------------------------------------------- /httpserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/acgshare/acgsh/db" 6 | "github.com/acgshare/acgsh/rpc" 7 | "github.com/acgshare/acgsh/search" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | posts_per_page = 80 16 | ) 17 | 18 | var httpPort string = "8080" 19 | 20 | func handler(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 22 | myItems := []string{r.Proto, "item2", r.URL.Path[1:]} 23 | a, _ := json.Marshal(myItems) 24 | 25 | w.Write(a) 26 | 27 | } 28 | func homeHandler(w http.ResponseWriter, r *http.Request) { 29 | if len(r.URL.Path[1:]) == 0 { 30 | http.ServeFile(w, r, "acgsh_html/index.html") 31 | return 32 | } 33 | http.ServeFile(w, r, "acgsh_html/"+r.URL.Path[1:]) 34 | 35 | } 36 | func getPostsHandler(w http.ResponseWriter, r *http.Request) { 37 | ss := r.URL.Path[11:] 38 | u, _ := strconv.ParseUint(ss, 10, 32) 39 | data, err := db.GetPosts(uint(u)*posts_per_page, posts_per_page) 40 | if err != nil { 41 | log.Println("Error: getPostsHandler", err) 42 | http.Error(w, "getPostsHandler db err", 500) 43 | return 44 | } 45 | 46 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 47 | w.Write(data) 48 | 49 | } 50 | func getCategoryPostsHandler(w http.ResponseWriter, r *http.Request) { 51 | ss := r.URL.Path[19:] 52 | 53 | params := strings.Split(ss, "/") 54 | 55 | if len(params) < 2 { 56 | http.Error(w, "Invalid params", 500) 57 | return 58 | } 59 | if len(params[0]) == 0 || len(params[0]) > 50 || len(params[1]) == 0 { 60 | http.Error(w, "Invalid params", 500) 61 | return 62 | } 63 | u, _ := strconv.ParseUint(params[1], 10, 32) 64 | data, err := db.GetCategoryPosts(params[0], uint(u)*posts_per_page, posts_per_page) 65 | if err != nil { 66 | log.Println("Error: getCategoryPostsHandler", err) 67 | http.Error(w, "getCategoryPostsHandler db err", 500) 68 | return 69 | } 70 | 71 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 72 | w.Write(data) 73 | 74 | } 75 | func getPubPostsHandler(w http.ResponseWriter, r *http.Request) { 76 | ss := r.URL.Path[14:] 77 | params := strings.Split(ss, "/") 78 | 79 | if len(params) < 2 { 80 | http.Error(w, "Invalid params", 500) 81 | return 82 | } 83 | if len(params[0]) == 0 || len(params[0]) > 16 || len(params[1]) == 0 { 84 | http.Error(w, "Invalid params", 500) 85 | return 86 | } 87 | u, _ := strconv.ParseUint(params[1], 10, 32) 88 | data, err := db.GetPubPosts(params[0], uint(u)*posts_per_page, posts_per_page) 89 | if err != nil { 90 | log.Println("Error: getCategoryPostsHandler", err) 91 | http.Error(w, "getCategoryPostsHandler db err", 500) 92 | return 93 | } 94 | 95 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 96 | w.Write(data) 97 | 98 | } 99 | 100 | func searchHandler(w http.ResponseWriter, r *http.Request) { 101 | ss := r.URL.Path[12:] 102 | params := strings.SplitN(ss, "/",2) 103 | 104 | if len(params) < 2 { 105 | http.Error(w, "Invalid params", 500) 106 | return 107 | } 108 | if len(params[0]) == 0 || len(params[1]) == 0 { 109 | http.Error(w, "Invalid params", 500) 110 | return 111 | } 112 | 113 | u, _ := strconv.Atoi(params[0]) 114 | 115 | searchStr:="" 116 | terms := strings.Split(params[1], " ") 117 | for _, term := range terms { 118 | //fmt.Println(term) 119 | if len(term)<1{ 120 | continue 121 | } 122 | if term[:1]=="+"{ 123 | if len(term[1:]) == 0 { 124 | continue 125 | } 126 | searchStr+=(" +\""+term[1:]+"\"") 127 | continue 128 | } 129 | if term[:1]=="-"{ 130 | if len(term[1:]) == 0 { 131 | continue 132 | } 133 | searchStr+=(" -\""+term[1:]+"\"") 134 | continue 135 | } 136 | searchStr+=(" +\""+term+"\"") 137 | 138 | } 139 | //fmt.Println(searchStr) 140 | 141 | ids, err := search.Search(searchStr,posts_per_page,u*posts_per_page) 142 | if err != nil { 143 | //log.Println("Error: searchHandler", err) 144 | http.Error(w, "searchHandler search err", 500) 145 | return 146 | } 147 | 148 | data, err := db.GetPostsWithIds(ids) 149 | if err != nil { 150 | log.Println("Error: searchHandler", err) 151 | http.Error(w, "searchHandler db err", 500) 152 | return 153 | } 154 | 155 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 156 | w.Write(data) 157 | 158 | } 159 | 160 | func getPubReplyHandler(w http.ResponseWriter, r *http.Request) { 161 | ss := r.URL.Path[14:] 162 | 163 | params := strings.Split(ss, "/") 164 | 165 | if len(params) < 2 { 166 | http.Error(w, "Invalid params", 500) 167 | return 168 | } 169 | if len(params[0]) == 0 || len(params[1]) == 0 { 170 | http.Error(w, "Invalid params", 500) 171 | return 172 | } 173 | 174 | data, err := db.GetPublishersReplyPosts(params[0], params[1]) 175 | if err != nil { 176 | log.Println("Error: getPubReplyHandler", err) 177 | http.Error(w, "getPubReplyHandler db err", 500) 178 | return 179 | } 180 | 181 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 182 | w.Write(data) 183 | 184 | } 185 | func getPublishersHandler(w http.ResponseWriter, r *http.Request) { 186 | 187 | publishers, err := rpc.GetFollowing(adminTwisterUsername) 188 | if err != nil { 189 | log.Println("Error: getPublishersHandler can not fetch following users for", adminTwisterUsername, "from Twister RPC server.", err) 190 | http.Error(w, "getPublishersHandler RPC err", 500) 191 | return 192 | } 193 | 194 | data, _ := json.Marshal(publishers) 195 | 196 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 197 | w.Write(data) 198 | 199 | } 200 | func regPublisherHandler(w http.ResponseWriter, r *http.Request) { 201 | if r.Method != "POST" { 202 | http.Error(w, "Invalid request method.", 405) 203 | return 204 | } 205 | ss := r.URL.Path[9:] 206 | 207 | if len(ss) < 1 || len(ss) > 16 { 208 | http.Error(w, "Invalid params", 404) 209 | return 210 | } 211 | 212 | result, err := rpc.Follow(adminTwisterUsername, []string{ss}) 213 | if err != nil { 214 | log.Println("Error: regPublisherHandler can not follow user", result, err) 215 | http.Error(w, "regPublisherHandler RPC err", 500) 216 | return 217 | } 218 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 219 | w.Write([]byte("{\"ok\" : true}")) 220 | 221 | } 222 | func startHttpServer() { 223 | //http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp")))) 224 | //http.Error(w, http.StatusText(500), 500) 225 | http.HandleFunc("/api/", handler) 226 | http.HandleFunc("/api/posts/", getPostsHandler) 227 | http.HandleFunc("/api/categoryposts/", getCategoryPostsHandler) 228 | http.HandleFunc("/api/pubposts/", getPubPostsHandler) 229 | http.HandleFunc("/api/search/", searchHandler) 230 | 231 | http.HandleFunc("/api/pubreply/", getPubReplyHandler) 232 | http.HandleFunc("/api/publishers/", getPublishersHandler) 233 | http.HandleFunc("/api/reg/", regPublisherHandler) 234 | http.HandleFunc("/", homeHandler) 235 | log.Println("Starting http server...") 236 | log.Fatal(http.ListenAndServe(":"+config.HttpServerPort, nil)) 237 | } 238 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/BurntSushi/toml" 7 | "github.com/acgshare/acgsh/db" 8 | "github.com/acgshare/acgsh/rpc" 9 | "github.com/acgshare/acgsh/search" 10 | ) 11 | 12 | var adminTwisterUsername string 13 | var config acgshConfig 14 | 15 | const ( 16 | max_post_id = 99999999 17 | ) 18 | 19 | type acgshConfig struct { 20 | TwisterUsername string 21 | TwisterServer string 22 | HttpServerPort string 23 | } 24 | 25 | func main() { 26 | log.SetFlags(log.LstdFlags | log.Lshortfile) 27 | 28 | // Load config 29 | 30 | if _, err := toml.DecodeFile("acgsh.conf", &config); err != nil { 31 | log.Fatalln("Error: can not load acgsh.conf", err) 32 | return 33 | } 34 | adminTwisterUsername = config.TwisterUsername 35 | 36 | //Init DB 37 | db.Init() 38 | defer db.Close() 39 | 40 | //Init search engine 41 | search.Init() 42 | 43 | rpc.SetAddress(config.TwisterServer) 44 | 45 | go runSyncTimeLine() 46 | 47 | //btih, category, fileSize, title, ok := retrieveMagnetInfo("#acgsh maGnet:? dn = =& xt=urn:btih:A3TU7P63QSNXXSYN2PDQYDZV4IYRU2CG& x.C = 動畫 &xl=123124&dn=[诸神字幕组][高校星歌剧][High School Star Musical][12][繁日双语字幕][720P][CHT MP4]") 48 | //println(btih, category, fileSize, title, ok) 49 | 50 | startHttpServer() 51 | } 52 | 53 | // todo: httpjsonrpcClient unmarshal json error 54 | // todo: httpjsonrpcClient no connection error 55 | // todo: httpjsonrpcClient log.fatal modify 56 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "fmt" 8 | "github.com/acgshare/Go-HTTP-JSON-RPC/httpjsonrpc" 9 | ) 10 | 11 | var Address string 12 | var id int64 13 | 14 | func SetAddress(newAddress string) { 15 | Address = newAddress 16 | } 17 | 18 | func Follow(name string, followUserNames []string) (interface{}, error) { 19 | // 20 | id++ 21 | resp, err := httpjsonrpc.Call(Address, "follow", id, []interface{}{name, followUserNames}) 22 | if err != nil { 23 | log.Println(err) 24 | log.Printf("Error: RPC Follow: %+v", resp) 25 | return nil, err 26 | } 27 | 28 | _ = resp.Result 29 | if resp.Error != nil { 30 | log.Printf("Error: RPC Follow: %+v", resp) 31 | return nil, fmt.Errorf("RPC error") 32 | } 33 | 34 | return resp, err 35 | } 36 | 37 | func UnFollow(name string, followUserNames []string) (interface{}, error) { 38 | // 39 | id++ 40 | resp, err := httpjsonrpc.Call(Address, "unfollow", id, []interface{}{name, followUserNames}) 41 | if err != nil { 42 | log.Println(err) 43 | return resp, err 44 | } 45 | result := resp.Result 46 | //log.Println(resp) 47 | 48 | return result, err 49 | } 50 | 51 | func GetFollowing(name string) (*[]string, error) { 52 | // 53 | id++ 54 | resp, err := httpjsonrpc.Call(Address, "getfollowing", id, []interface{}{name}) 55 | if err != nil { 56 | log.Println(err) 57 | log.Printf("Error: RPC GetFollowing: %+v", resp) 58 | return nil, err 59 | } 60 | result := resp.Result 61 | if resp.Error != nil { 62 | log.Printf("Error: RPC GetFollowing: %+v", resp) 63 | return nil, fmt.Errorf("RPC error") 64 | } 65 | 66 | var following []string 67 | err = json.Unmarshal(result, &following) 68 | if err != nil { 69 | log.Println(err) 70 | log.Printf("Error: RPC GetFollowing Unmarshal: %+v", result) 71 | return nil, err 72 | } 73 | 74 | return &following, nil 75 | } 76 | func ListWalletUsers() (interface{}, error) { 77 | // 78 | id++ 79 | resp, err := httpjsonrpc.Call(Address, "listwalletusers", id, nil) 80 | if err != nil { 81 | log.Println(err) 82 | return resp, err 83 | } 84 | result := resp.Result 85 | //log.Println(resp) 86 | 87 | return result, err 88 | } 89 | func GetPosts(count int64, params []interface{}) (*TwisterPosts, error) { 90 | // 91 | id++ 92 | resp, err := httpjsonrpc.Call(Address, "getposts", id, []interface{}{count, params}) 93 | if err != nil { 94 | log.Println(err) 95 | return nil, err 96 | } 97 | result := resp.Result 98 | if resp.Error != nil { 99 | log.Printf("Error: RPC GetPosts: %+v", resp) 100 | return nil, fmt.Errorf("RPC error") 101 | } 102 | //fmt.Println(string(result)) 103 | var tp TwisterPosts 104 | err = json.Unmarshal(result, &tp) 105 | if err != nil { 106 | log.Println(err) 107 | log.Printf("Error: RPC GetPosts Unmarshal: %+v", result) 108 | return nil, err 109 | } 110 | 111 | return &tp, nil 112 | } 113 | -------------------------------------------------------------------------------- /rpc/types.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | type TwisterPost struct { 4 | SigUserpost string `json:"sig_userpost"` 5 | Userpost struct { 6 | Height int64 `json:"height"` 7 | K int64 `json:"k"` 8 | Lastk *int64 `json:"lastk"` 9 | Msg string `json:"msg"` 10 | N string `json:"n"` 11 | Reply *struct { 12 | K int64 `json:"k"` 13 | N string `json:"n"` 14 | } `json:"reply"` 15 | Rt *struct { 16 | Height int64 `json:"height"` 17 | K int64 `json:"k"` 18 | Lastk int64 `json:"lastk"` 19 | Msg string `json:"msg"` 20 | N string `json:"n"` 21 | Reply struct { 22 | K int64 `json:"k"` 23 | N string `json:"n"` 24 | } `json:"reply"` 25 | Time int64 `json:"time"` 26 | } `json:"rt"` 27 | SigRt string `json:"sig_rt"` 28 | Time uint64 `json:"time"` 29 | } `json:"userpost"` 30 | } 31 | 32 | type TwisterPosts []TwisterPost 33 | -------------------------------------------------------------------------------- /search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | 7 | "github.com/acgshare/acgsh/db" 8 | "github.com/acgshare/bleve" 9 | ) 10 | 11 | var index bleve.Index 12 | 13 | func Init() { 14 | // open a new index 15 | var err error 16 | index, err = bleve.Open("acgsh.bleve") 17 | if err != nil { 18 | log.Println(err) 19 | mapping := bleve.NewIndexMapping() 20 | index, err = bleve.New("acgsh.bleve", mapping) 21 | if err != nil { 22 | log.Println(err) 23 | return 24 | } 25 | } 26 | 27 | } 28 | 29 | func Index(posts *db.ShPosts) { 30 | b := index.NewBatch() 31 | for _, sp := range *posts { 32 | id := db.Ui64tob(sp.Time) 33 | id = append(id, ":"...) 34 | id = append(id, sp.N...) 35 | id = append(id, ":"...) 36 | id = append(id, strconv.FormatInt(sp.K, 10)...) 37 | 38 | data := struct { 39 | Title string 40 | }{ 41 | Title: sp.Title, 42 | } 43 | 44 | err := b.Index(string(id), data) 45 | if err != nil { 46 | log.Printf("Error: search engine Index: %+v %+v\n", id, data) 47 | } 48 | } 49 | err := index.Batch(b) 50 | if err != nil { 51 | log.Printf("Error: search engine index.Batch(): %+v \n", posts) 52 | } 53 | } 54 | 55 | func Search(ss string, size, from int) ([][]byte, error) { 56 | query := bleve.NewQueryStringQuery(ss) 57 | search := bleve.NewSearchRequestOptions(query, size, from, false) 58 | searchResults, err := index.Search(search) 59 | if err != nil { 60 | log.Println("Error: search engine Search(): %s \n", err) 61 | return [][]byte{}, err 62 | } 63 | //fmt.Println(searchResults) 64 | ids := [][]byte{} 65 | 66 | for i := 0; i < searchResults.Hits.Len(); i = i + 1 { 67 | if searchResults.Hits[i] != nil { 68 | ids = append(ids, []byte(searchResults.Hits[i].ID)) 69 | } 70 | } 71 | 72 | return ids, nil 73 | } 74 | -------------------------------------------------------------------------------- /timeline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "time" 7 | 8 | "strings" 9 | 10 | "github.com/acgshare/acgsh/db" 11 | "github.com/acgshare/acgsh/rpc" 12 | "github.com/acgshare/acgsh/search" 13 | ) 14 | 15 | const sync_posts_number = 2000 16 | 17 | type updatePostsInfo struct { 18 | maxK int64 19 | minK int64 20 | lastK int64 21 | } 22 | 23 | func min(a, b int64) int64 { 24 | if a < b { 25 | return a 26 | } 27 | return b 28 | } 29 | 30 | func syncTimeLine() { 31 | // Update publishers 32 | twisterUsers, err := rpc.GetFollowing(adminTwisterUsername) 33 | if err != nil { 34 | log.Println("Error: can not fetch following users for", adminTwisterUsername, "from Twister RPC server.", err) 35 | } 36 | log.Println(twisterUsers) 37 | log.Println("Get", len(*twisterUsers), "following users for", adminTwisterUsername) 38 | db.AddPublishersIfNotExist(twisterUsers) 39 | 40 | // Get all publishers' username, max id, since id and latest id (k), from DB 41 | publishers, err := db.GetPublishers() 42 | if err != nil { 43 | log.Println(err) 44 | log.Printf("Error: syncTimeLine GetPublishers: %+v\n", publishers) 45 | } 46 | //log.Printf("%+v\n", publishers) 47 | 48 | // Get posts from Twister for all publishers 49 | var gup getUserPosts 50 | for name, data := range publishers { 51 | gup.addUser(name, data.Max, data.Since) 52 | } 53 | 54 | newPosts, err := rpc.GetPosts(sync_posts_number, gup.data) 55 | //log.Printf("%+v\n", presult) 56 | if err != nil { 57 | log.Println(err) 58 | log.Printf("Error: syncTimeLine GetPosts: %+v\n", gup.data) 59 | return 60 | } 61 | log.Println(len(*newPosts), "new posts") 62 | 63 | // Update publishers sync data with info from retrieved posts. 64 | var newShPosts db.ShPosts 65 | var newShPubReplyPosts db.ShPubReplyPosts 66 | hasNewPostsPublishers := make(map[string]updatePostsInfo) 67 | for _, tp := range *newPosts { 68 | 69 | // Make sure current post's username is in the publishers list.(May contain promoted post) 70 | _, ok := publishers[tp.Userpost.N] 71 | if !ok { 72 | continue 73 | } 74 | 75 | // Get lastK, may be nil pointer. 76 | var lastK int64 77 | if tp.Userpost.Lastk == nil { 78 | lastK = -1 79 | } else { 80 | lastK = *(tp.Userpost.Lastk) 81 | } 82 | 83 | upi, ok := hasNewPostsPublishers[tp.Userpost.N] 84 | if ok { 85 | upi.lastK = lastK 86 | if upi.maxK < tp.Userpost.K { 87 | upi.maxK = tp.Userpost.K 88 | } 89 | if upi.minK > tp.Userpost.K { 90 | upi.minK = tp.Userpost.K 91 | } 92 | hasNewPostsPublishers[tp.Userpost.N] = upi 93 | } else { 94 | hasNewPostsPublishers[tp.Userpost.N] = updatePostsInfo{ 95 | lastK: lastK, 96 | maxK: tp.Userpost.K, 97 | minK: tp.Userpost.K, 98 | } 99 | 100 | } 101 | 102 | // Add New ShPost 103 | if tp.Userpost.Reply == nil && tp.Userpost.Rt == nil { 104 | btih, category, fileSize, title, ok := retrieveMagnetInfo(tp.Userpost.Msg) 105 | if ok { 106 | shPost := db.ShPost{ 107 | Msg: tp.Userpost.Msg, 108 | N: tp.Userpost.N, 109 | K: tp.Userpost.K, 110 | Lastk: lastK, 111 | Time: tp.Userpost.Time, 112 | Category: category, 113 | Title: title, 114 | Magnet: btih, 115 | Size: fileSize, 116 | } 117 | 118 | newShPosts = append(newShPosts, shPost) 119 | //fmt.Println(index, tp.Userpost.N, tp.Userpost.K, lastK) 120 | } 121 | } 122 | 123 | // Add new ShPubReplyPost 124 | if tp.Userpost.Reply != nil { 125 | if tp.Userpost.Reply.N == tp.Userpost.N { 126 | shPubReplyPosts := db.ShPubReplyPost{ 127 | Msg: tp.Userpost.Msg, 128 | Time: tp.Userpost.Time, 129 | N: tp.Userpost.N, 130 | K: tp.Userpost.Reply.K, 131 | Lastk: lastK, 132 | } 133 | newShPubReplyPosts = append(newShPubReplyPosts, shPubReplyPosts) 134 | //fmt.Println(index, tp.Userpost.N, tp.Userpost.K, lastK) 135 | } 136 | } 137 | } 138 | 139 | // Save posts to DB 140 | db.AddPosts(&newShPosts) 141 | 142 | err = db.AddPublishersReplyPosts(&newShPubReplyPosts) 143 | if err != nil { 144 | log.Println(err) 145 | log.Printf("Error: syncTimeLine db.AddPublishersReplyPosts: %+v\n", len(newShPubReplyPosts)) 146 | } 147 | 148 | //log.Printf("%+v\n", hasNewPostsPublishers) 149 | 150 | // Update publishers in DB 151 | updatedPublishers := make(map[string]db.SyncData) 152 | for name, postsInfo := range hasNewPostsPublishers { 153 | var newSd db.SyncData 154 | oldSd := publishers[name] 155 | 156 | newSd.Latest = oldSd.Latest 157 | if postsInfo.maxK > oldSd.Latest { 158 | newSd.Latest = postsInfo.maxK 159 | } 160 | 161 | newSd.Since = oldSd.Since 162 | lastK := min(postsInfo.lastK, postsInfo.minK-1) 163 | if lastK <= oldSd.Since { 164 | newSd.Since = newSd.Latest 165 | newSd.Max = max_post_id 166 | } else { 167 | newSd.Max = lastK 168 | } 169 | 170 | updatedPublishers[name] = newSd 171 | } 172 | 173 | err = db.UpdatePublishers(&updatedPublishers) 174 | if err != nil { 175 | log.Println(err) 176 | log.Printf("Error: syncTimeLine: %+v\n", updatedPublishers) 177 | } 178 | 179 | // Indexing posts 180 | search.Index(&newShPosts) 181 | 182 | } 183 | 184 | func retrieveMagnetInfo(ss string) (string, string, uint64, string, bool) { 185 | 186 | lowerS := strings.ToLower(ss) 187 | // If not magnet return 188 | if !(strings.Contains(lowerS, "magnet")) { 189 | return "", "", 0, "", false 190 | } 191 | 192 | idx := strings.Index(lowerS, "magnet") 193 | 194 | magnetString := ss[idx:] 195 | 196 | idx = strings.Index(magnetString, "?") 197 | if idx == -1 { 198 | return "", "", 0, "", false 199 | } 200 | if len(magnetString) <= idx+1 { 201 | return "", "", 0, "", false 202 | } 203 | paramString := magnetString[idx+1:] 204 | //log.Println(paramString) 205 | 206 | params := strings.Split(paramString, "&") 207 | 208 | var category, title string 209 | var fileSize uint64 210 | 211 | for _, param := range params { 212 | //log.Println(param) 213 | k := strings.Split(param, "=") 214 | //log.Println(k, len(k)) 215 | if len(k) >= 2 { 216 | n := strings.ToLower(k[0]) 217 | if strings.Contains(n, "xl") { 218 | var err error 219 | fileSize, err = strconv.ParseUint(k[1], 10, 64) 220 | if err != nil { 221 | fileSize = 0 222 | } 223 | } 224 | if strings.Contains(n, "x.c") { 225 | category = k[1] 226 | } 227 | if strings.Contains(n, "dn") { 228 | title = k[1] 229 | } 230 | } 231 | } 232 | 233 | magnetString = strings.TrimSpace(magnetString) 234 | category = strings.TrimSpace(category) 235 | title = strings.TrimSpace(title) 236 | 237 | return magnetString, category, fileSize, title, true 238 | } 239 | 240 | func runSyncTimeLine() { 241 | for { 242 | time.Sleep(60 * time.Second) 243 | syncTimeLine() 244 | 245 | } 246 | } 247 | 248 | // todo: add x.l 249 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type userGetPosts struct { 4 | Username string `json:"username"` 5 | Max_id int64 `json:"max_id"` 6 | Since_id int64 `json:"since_id"` 7 | } 8 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type getUserPosts struct { 4 | data []interface{} 5 | } 6 | 7 | func (g *getUserPosts) addUser(username string, max, since int64) { 8 | 9 | g.data = append(g.data, userGetPosts{ 10 | Username: username, 11 | Max_id: max, 12 | Since_id: since, 13 | }) 14 | } 15 | --------------------------------------------------------------------------------