GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

592 lines
14 KiB

  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/rpdata/api/internal/auth"
  6. "git.aiterp.net/rpdata/api/models"
  7. "git.aiterp.net/rpdata/api/models/changekeys"
  8. "git.aiterp.net/rpdata/api/models/channels"
  9. "git.aiterp.net/rpdata/api/repositories"
  10. "git.aiterp.net/rpdata/api/services/parsers"
  11. "golang.org/x/sync/errgroup"
  12. "log"
  13. "strings"
  14. "time"
  15. )
  16. type LogService struct {
  17. logs repositories.LogRepository
  18. posts repositories.PostRepository
  19. changeService *ChangeService
  20. channelService *ChannelService
  21. characterService *CharacterService
  22. }
  23. func (s *LogService) Find(ctx context.Context, id string) (*models.Log, error) {
  24. return s.logs.Find(ctx, id)
  25. }
  26. func (s *LogService) FindPosts(ctx context.Context, id string) (*models.Post, error) {
  27. return s.posts.Find(ctx, id)
  28. }
  29. func (s *LogService) List(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
  30. if filter == nil {
  31. filter = &models.LogFilter{}
  32. }
  33. return s.logs.List(ctx, *filter)
  34. }
  35. func (s *LogService) ListPosts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
  36. // Some sanity checks to avoid querying an insame amount of posts.
  37. if filter == nil {
  38. filter = &models.PostFilter{Limit: 100}
  39. } else {
  40. if (filter.Limit <= 0 || filter.Limit > 512) && (filter.LogID == nil && len(filter.IDs) == 0) {
  41. return nil, errors.New("a limit of 0 (no limit) or >512 without a logId or a set of IDs is not allowed")
  42. }
  43. if len(filter.IDs) > 100 {
  44. return nil, errors.New("you may not query for more than 100 ids, split your query")
  45. }
  46. }
  47. return s.posts.List(ctx, *filter)
  48. }
  49. func (s *LogService) Create(ctx context.Context, title, description, channelName, eventName string, open bool) (*models.Log, error) {
  50. if channelName == "" {
  51. return nil, errors.New("channel name cannot be empty")
  52. }
  53. log := &models.Log{
  54. Title: title,
  55. Description: description,
  56. ChannelName: channelName,
  57. EventName: eventName,
  58. Date: time.Now(),
  59. Open: open,
  60. }
  61. if err := auth.CheckPermission(ctx, "add", log); err != nil {
  62. return nil, err
  63. }
  64. _, err := s.channelService.Ensure(ctx, channelName)
  65. if err != nil {
  66. return nil, err
  67. }
  68. log, err = s.logs.Insert(ctx, *log)
  69. if err != nil {
  70. return nil, err
  71. }
  72. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  73. return log, nil
  74. }
  75. // Import creates new logs from common formats.
  76. func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
  77. if err := auth.CheckPermission(ctx, "add", &models.Log{}); err != nil {
  78. return nil, err
  79. }
  80. results := make([]*models.Log, 0, 8)
  81. _, err := s.channelService.Ensure(ctx, channelName)
  82. if err != nil {
  83. return nil, err
  84. }
  85. eventName := ""
  86. if channel, err := channels.FindName(channelName); err == nil {
  87. eventName = channel.EventName
  88. }
  89. date = date.In(tz)
  90. switch importer {
  91. case models.LogImporterMircLike:
  92. {
  93. if date.IsZero() {
  94. return nil, errors.New("date is not optional for mirc-like logs")
  95. }
  96. parsed, err := parsers.MircLog(data, date, true)
  97. if err != nil {
  98. return nil, err
  99. }
  100. parsed.Log.EventName = eventName
  101. parsed.Log.ChannelName = channelName
  102. log, err := s.logs.Insert(ctx, parsed.Log)
  103. if err != nil {
  104. return nil, err
  105. }
  106. for _, post := range parsed.Posts {
  107. post.LogID = log.ShortID
  108. }
  109. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  110. if err != nil {
  111. _ = s.logs.Delete(ctx, *log)
  112. return nil, err
  113. }
  114. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  115. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  116. results = append(results, log)
  117. }
  118. case models.LogImporterForumLog:
  119. {
  120. parseResults, err := parsers.ForumLog(data, tz)
  121. if err != nil {
  122. return nil, err
  123. }
  124. for _, parsed := range parseResults {
  125. log, err := s.logs.Insert(ctx, parsed.Log)
  126. if err != nil {
  127. return nil, err
  128. }
  129. parsed.Log.EventName = eventName
  130. parsed.Log.ChannelName = channelName
  131. for _, post := range parsed.Posts {
  132. post.LogID = log.ShortID
  133. }
  134. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  135. if err != nil {
  136. _ = s.logs.Delete(ctx, *log)
  137. return nil, err
  138. }
  139. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  140. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  141. }
  142. }
  143. default:
  144. {
  145. return nil, errors.New("Invalid importer: " + importer.String())
  146. }
  147. }
  148. return results, nil
  149. }
  150. func (s *LogService) Update(ctx context.Context, id string, update models.LogUpdate) (*models.Log, error) {
  151. log, err := s.logs.Find(ctx, id)
  152. if err != nil {
  153. return nil, err
  154. }
  155. if err := auth.CheckPermission(ctx, "edit", log); err != nil {
  156. return nil, err
  157. }
  158. log, err = s.logs.Update(ctx, *log, update)
  159. if err != nil {
  160. return nil, err
  161. }
  162. s.changeService.Submit(ctx, models.ChangeModelLog, "edit", true, changekeys.Listed(log), log)
  163. return log, nil
  164. }
  165. func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time, kind, nick, text string) (*models.Post, error) {
  166. if kind == "" || nick == "" || time.IsZero() {
  167. return nil, errors.New("kind, nick and time must be non-empty")
  168. }
  169. l, err := s.logs.Find(ctx, logId)
  170. if err != nil {
  171. return nil, err
  172. }
  173. post := &models.Post{
  174. LogID: l.ShortID,
  175. Kind: kind,
  176. Nick: nick,
  177. Text: text,
  178. Time: time,
  179. }
  180. if err := auth.CheckPermission(ctx, "add", post); err != nil {
  181. return nil, err
  182. }
  183. post, err = s.posts.Insert(ctx, *post)
  184. if err != nil {
  185. return nil, err
  186. }
  187. err = s.refreshLogCharacters(ctx, *l, nil)
  188. if err != nil {
  189. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  190. }
  191. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(l, post), post)
  192. return post, nil
  193. }
  194. func (s *LogService) EditPost(ctx context.Context, id string, update models.PostUpdate) (*models.Post, error) {
  195. if (update.Kind != nil && *update.Kind == "") || (update.Nick != nil && *update.Nick == "") || (update.Text != nil && *update.Text == "") {
  196. return nil, errors.New("kind, nick and time must be non-empty")
  197. }
  198. post, err := s.posts.Find(ctx, id)
  199. if err != nil {
  200. return nil, err
  201. }
  202. if err := auth.CheckPermission(ctx, "edit", post); err != nil {
  203. return nil, err
  204. }
  205. post, err = s.posts.Update(ctx, *post, update)
  206. if err != nil {
  207. return nil, err
  208. }
  209. go func() {
  210. l, err := s.logs.Find(context.Background(), post.LogID)
  211. if err != nil {
  212. return
  213. }
  214. err = s.refreshLogCharacters(ctx, *l, nil)
  215. if err != nil {
  216. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  217. }
  218. s.changeService.Submit(ctx, models.ChangeModelPost, "edit", true, changekeys.Many(l, post), post)
  219. }()
  220. return post, nil
  221. }
  222. func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
  223. if position < 1 {
  224. return nil, repositories.ErrInvalidPosition
  225. }
  226. post, err := s.posts.Find(ctx, id)
  227. if err != nil {
  228. return nil, err
  229. }
  230. if err := auth.CheckPermission(ctx, "move", post); err != nil {
  231. return nil, err
  232. }
  233. posts, err := s.posts.Move(ctx, *post, position)
  234. if err != nil {
  235. return nil, err
  236. }
  237. go func() {
  238. if len(posts) == 0 {
  239. return
  240. }
  241. log, err := s.logs.Find(context.Background(), posts[0].LogID)
  242. if err != nil {
  243. return
  244. }
  245. s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
  246. }()
  247. return posts, nil
  248. }
  249. func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
  250. post, err := s.posts.Find(ctx, id)
  251. if err != nil {
  252. return nil, err
  253. }
  254. if err := auth.CheckPermission(ctx, "remove", post); err != nil {
  255. return nil, err
  256. }
  257. err = s.posts.Delete(ctx, *post)
  258. if err != nil {
  259. return nil, err
  260. }
  261. go func() {
  262. l, err := s.logs.Find(context.Background(), post.LogID)
  263. if err != nil {
  264. return
  265. }
  266. err = s.refreshLogCharacters(ctx, *l, nil)
  267. if err != nil {
  268. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  269. }
  270. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, post), post)
  271. }()
  272. return post, nil
  273. }
  274. func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
  275. log, err := s.logs.Find(ctx, id)
  276. if err != nil {
  277. return nil, err
  278. }
  279. if err := auth.CheckPermission(ctx, "remove", log); err != nil {
  280. return nil, err
  281. }
  282. err = s.logs.Delete(ctx, *log)
  283. if err != nil {
  284. return nil, err
  285. }
  286. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
  287. return log, nil
  288. }
  289. func (s *LogService) FixImportDateBug(ctx context.Context) error {
  290. start := time.Now()
  291. logs, err := s.logs.List(ctx, models.LogFilter{})
  292. if err != nil {
  293. return err
  294. }
  295. eg := errgroup.Group{}
  296. for i := range logs {
  297. l := logs[i]
  298. eg.Go(func() error {
  299. return s.fixImportDateBug(ctx, *l)
  300. })
  301. }
  302. err = eg.Wait()
  303. if err != nil {
  304. return err
  305. }
  306. log.Printf("Date import bug check finished: logs: %d, duration: %s", len(logs), time.Since(start))
  307. return nil
  308. }
  309. func (s *LogService) fixImportDateBug(ctx context.Context, l models.Log) error {
  310. // Find the log's posts.
  311. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  312. if err != nil {
  313. return err
  314. }
  315. if len(posts) < 8 {
  316. return nil
  317. }
  318. // Find first action post
  319. first := posts[0]
  320. fi := 0
  321. for first.Kind != "action" && first.Kind != "text" {
  322. fi++
  323. if fi >= len(posts) {
  324. return nil
  325. }
  326. first = posts[fi]
  327. }
  328. last := posts[len(posts)-1]
  329. if first == last {
  330. return nil
  331. }
  332. // Stop here if this log probably isn't affected
  333. if last.Time.Sub(first.Time) < time.Hour*72 {
  334. return nil
  335. }
  336. // Find the first post past midnight.
  337. midnight := first
  338. mi := fi
  339. for i, post := range posts[fi+1:] {
  340. if post.Time.Hour() < first.Time.Hour() {
  341. midnight = post
  342. mi = fi + 1 + i
  343. break
  344. }
  345. }
  346. if midnight == last {
  347. return nil
  348. }
  349. if len(posts[mi+1:]) == 1 {
  350. return nil
  351. }
  352. hits := 0
  353. prev := midnight
  354. for _, offender := range posts[mi+1:] {
  355. if offender.Time.Day() != prev.Time.Day() {
  356. hits += 1
  357. }
  358. prev = offender
  359. }
  360. if hits < ((len(posts[mi+1:]) * 3) / 4) {
  361. return nil
  362. }
  363. for _, offender := range posts[mi+1:] {
  364. ot := offender.Time.UTC()
  365. mt := midnight.Time.UTC()
  366. y, m, d := mt.Date()
  367. hr, mi, se, ns := ot.Hour(), ot.Minute(), ot.Second(), ot.Nanosecond()
  368. newTime := time.Date(y, m, d, hr, mi, se, ns, time.UTC)
  369. _, err := s.posts.Update(ctx, *offender, models.PostUpdate{Time: &newTime})
  370. if err != nil {
  371. return err
  372. }
  373. }
  374. log.Printf("Fixed import date bug in %d posts in log %s", len(posts[mi+1:]), l.ID)
  375. return nil
  376. }
  377. func (s *LogService) RefreshAllLogCharacters(ctx context.Context) error {
  378. start := time.Now()
  379. // Get all logs
  380. logs, err := s.logs.List(ctx, models.LogFilter{})
  381. if err != nil {
  382. return err
  383. }
  384. // Check all characters now instead of later.
  385. characters, err := s.characterService.List(ctx, models.CharacterFilter{})
  386. if err != nil {
  387. return err
  388. }
  389. characterMap := s.makeCharacterMap(characters)
  390. eg := errgroup.Group{}
  391. for i := range logs {
  392. l := logs[i]
  393. eg.Go(func() error {
  394. return s.refreshLogCharacters(ctx, *l, characterMap)
  395. })
  396. }
  397. err = eg.Wait()
  398. if err != nil {
  399. return err
  400. }
  401. log.Printf("Full log character refresh complete; nicks: %d, logs: %d, duration: %s", len(characterMap), len(logs), time.Since(start))
  402. return nil
  403. }
  404. func (s *LogService) RefreshLogCharacters(ctx context.Context, log models.Log) error {
  405. return s.refreshLogCharacters(ctx, log, nil)
  406. }
  407. func (s *LogService) refreshLogCharacters(ctx context.Context, log models.Log, characterMap map[string]*models.Character) error {
  408. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
  409. if err != nil {
  410. return nil
  411. }
  412. counts := make(map[string]int)
  413. added := make(map[string]bool)
  414. removed := make(map[string]bool)
  415. for _, post := range posts {
  416. if post.Kind == "text" || post.Kind == "action" {
  417. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") || strings.HasSuffix(post.Nick, "|") {
  418. continue
  419. }
  420. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  421. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  422. post.Nick = post.Nick[:len(post.Nick)-2]
  423. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  424. post.Nick = post.Nick[:len(post.Nick)-1]
  425. }
  426. added[post.Nick] = true
  427. counts[post.Nick]++
  428. }
  429. if post.Kind == "chars" {
  430. tokens := strings.Fields(post.Text)
  431. for _, token := range tokens {
  432. if strings.HasPrefix(token, "-") {
  433. removed[token[1:]] = true
  434. } else {
  435. added[strings.Replace(token, "+", "", 1)] = true
  436. }
  437. }
  438. }
  439. }
  440. nicks := make([]string, 0, len(added))
  441. for nick := range added {
  442. if added[nick] && !removed[nick] {
  443. nicks = append(nicks, nick)
  444. }
  445. }
  446. if characterMap == nil {
  447. characters, err := s.characterService.List(ctx, models.CharacterFilter{Nicks: nicks})
  448. if err != nil {
  449. return err
  450. }
  451. characterMap = s.makeCharacterMap(characters)
  452. }
  453. log.CharacterIDs = log.CharacterIDs[:0]
  454. for key := range added {
  455. delete(added, key)
  456. }
  457. for _, nick := range nicks {
  458. character := characterMap[nick]
  459. if character == nil || added[character.ID] {
  460. continue
  461. }
  462. added[character.ID] = true
  463. log.CharacterIDs = append(log.CharacterIDs, character.ID)
  464. }
  465. _, err = s.logs.Update(ctx, log, models.LogUpdate{CharacterIDs: log.CharacterIDs})
  466. return err
  467. }
  468. func (s *LogService) makeCharacterMap(characters []*models.Character) map[string]*models.Character {
  469. characterMap := make(map[string]*models.Character, len(characters)*3)
  470. for _, character := range characters {
  471. for _, nick := range character.Nicks {
  472. characterMap[nick] = character
  473. }
  474. }
  475. return characterMap
  476. }